commit fe82a97930ed0e986dd16c82e39f777d8911bbc3
Author: Leszek Koltunski <leszek@distoretedandroid.org>
Date:   Wed Jun 7 18:18:58 2017 +0100

    Progress with support for Effect classes.

diff --git a/src/main/java/org/distorted/library/Distorted.java b/src/main/java/org/distorted/library/Distorted.java
deleted file mode 100644
index 86afec7..0000000
--- a/src/main/java/org/distorted/library/Distorted.java
+++ /dev/null
@@ -1,160 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ConfigurationInfo;
-import android.content.res.Resources;
-
-import org.distorted.library.effect.EffectMessageSender;
-import org.distorted.library.effect.EffectQueue;
-import org.distorted.library.effect.EffectQueuePostprocess;
-import org.distorted.library.program.*;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * A singleton class used to control various global settings.
- */
-public class Distorted 
-  {
-  static int GLSL;
-  static String GLSL_VERSION;
-  /**
-   * When creating an instance of a DistortedTexture from another instance, clone the Bitmap that's
-   * backing up our DistortedTexture.
-   * <p>
-   * This way we can have two DistortedTextures, both backed up by the same Bitmap, to which we can
-   * apply different effects. Used in the copy constructor.
-   */
-  public static final int CLONE_SURFACE = 0x1;
-  /**
-   * When creating an instance of a DistortedEffects from another instance, clone the Matrix Effects.
-   * <p>
-   * This way we can have two different DistortedEffects sharing the MATRIX queue.
-   */
-  public static final int CLONE_MATRIX = 0x2;
-  /**
-   * When creating an instance of a DistortedEffects from another instance, clone the Vertex Effects.
-   * <p>
-   * This way we can have two different DistortedEffects sharing the VERTEX queue.
-   */
-  public static final int CLONE_VERTEX  = 0x4;
-  /**
-   * When creating an instance of a DistortedEffects from another instance, clone the Fragment Effects.
-   * <p>
-   * This way we can have two different DistortedEffects sharing the FRAGMENT queue.
-   */
-  public static final int CLONE_FRAGMENT= 0x8;
-   /**
-   * When creating an instance of a DistortedEffects from another instance, clone the PostProcess Effects.
-   * <p>
-   * This way we can have two different DistortedEffects sharing the POSTPROCESS queue.
-   */
-  public static final int CLONE_POSTPROCESS= 0x10;
-  /**
-   * When creating an instance of a DistortedNode from another instance, clone the children Nodes.
-   * <p>
-   * This is mainly useful for creating many similar sub-trees and rendering then at different places
-   * on the screen with (optionally) different Effects.
-   */
-  public static final int CLONE_CHILDREN= 0x20;
-
-  private static boolean mInitialized=false;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// private: hide this from Javadoc
-
-  private Distorted()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Have we called onCreate yet, ie have we initialized the library?
- * @return <code>true</code> if the library is initilized and ready for action.
- */
-  public static boolean isInitialized()
-    {
-    return mInitialized;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When OpenGL context gets created, you need to call this method so that the library can initialise its internal data structures.
- * I.e. best called from GLSurfaceView.onCreate().
- * <p>
- * Needs to be called from a thread holding the OpenGL context.
- *   
- * @param context Context of the App using the library - used to open up Resources and read Shader code.
- * @throws FragmentCompilationException Fragment Shader failed to compile
- * @throws VertexCompilationException   Vertex Shader failed to compile
- * @throws VertexUniformsException      Too many uniforms in the Vertex Shader
- * @throws FragmentUniformsException    Too many uniforms in the Fragment Shader
- * @throws LinkingException             Shader failed to link
- */
-  public static void onCreate(final Context context)
-  throws FragmentCompilationException,VertexCompilationException,VertexUniformsException,FragmentUniformsException,LinkingException
-    {
-    final ActivityManager activityManager     = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-    final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
-    android.util.Log.e("DISTORTED", "Using OpenGL ES "+configurationInfo.getGlEsVersion());
-
-    GLSL = ( (configurationInfo.reqGlEsVersion>>16)>=3 ? 300 : 100 );
-    GLSL_VERSION= (GLSL==100 ? "#version 100\n" : "#version 300 es\n");
-
-    final Resources resources = context.getResources();
-    DistortedEffects.createProgram(resources);
-    EffectQueuePostprocess.createProgram(resources);
-    EffectMessageSender.startSending();
-
-    mInitialized = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Call this so that the Library can release its internal data structures.
- * Must be called from Activity.onPause().
- */
-  public static void onPause()
-    {
-    DistortedObject.onPause();
-    DistortedNode.onPause();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Call this so that the Library can release its internal data structures.
- * Must be called from Activity.onDestroy(). 
- */
-  public static void onDestroy()
-    {
-    DistortedObject.onDestroy();
-    DistortedNode.onDestroy();
-    DistortedEffects.onDestroy();
-    DistortedEffectsPostprocess.onDestroy();
-    DistortedMaster.onDestroy();
-    EffectQueue.onDestroy();
-    EffectMessageSender.stopSending();
-
-    mInitialized = false;
-    }
-  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/library/DistortedEffects.java b/src/main/java/org/distorted/library/DistortedEffects.java
deleted file mode 100644
index 4854804..0000000
--- a/src/main/java/org/distorted/library/DistortedEffects.java
+++ /dev/null
@@ -1,1090 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import android.content.res.Resources;
-import android.opengl.GLES30;
-
-import org.distorted.library.effect.EffectQueue;
-import org.distorted.library.effect.EffectQueueFragment;
-import org.distorted.library.effect.EffectQueueMatrix;
-import org.distorted.library.effect.EffectQueueVertex;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.program.DistortedProgram;
-import org.distorted.library.program.FragmentCompilationException;
-import org.distorted.library.program.FragmentUniformsException;
-import org.distorted.library.program.LinkingException;
-import org.distorted.library.program.VertexCompilationException;
-import org.distorted.library.program.VertexUniformsException;
-import org.distorted.library.type.Data1D;
-import org.distorted.library.type.Data2D;
-import org.distorted.library.type.Data3D;
-import org.distorted.library.type.Data4D;
-import org.distorted.library.type.Data5D;
-import org.distorted.library.type.Static3D;
-
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.FloatBuffer;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Class containing Matrix,Vertex and Fragment effect queues. Postprocessing queue is held in a separate
- * class.
- * <p>
- * The queues hold actual effects to be applied to a given (DistortedTexture,MeshObject) combo.
- */
-public class DistortedEffects
-  {
-  /// MAIN PROGRAM ///
-  private static DistortedProgram mMainProgram;
-  private static int mMainTextureH;
-  private static boolean[] mEffectEnabled = new boolean[EffectNames.size()];
-
-  static
-    {
-    int len = EffectNames.size();
-
-    for(int i=0; i<len; i++)
-      {
-      mEffectEnabled[i] = false;
-      }
-    }
-
-  /// BLIT PROGRAM ///
-  private static DistortedProgram mBlitProgram;
-  private static int mBlitTextureH;
-  private static int mBlitDepthH;
-  private static final FloatBuffer mQuadPositions;
-
-  static
-    {
-    float[] positionData= { -0.5f, -0.5f,  -0.5f, 0.5f,  0.5f,-0.5f,  0.5f, 0.5f };
-    mQuadPositions = ByteBuffer.allocateDirect(32).order(ByteOrder.nativeOrder()).asFloatBuffer();
-    mQuadPositions.put(positionData).position(0);
-    }
-
-  /// BLIT DEPTH PROGRAM ///
-  private static DistortedProgram mBlitDepthProgram;
-  private static int mBlitDepthTextureH;
-  private static int mBlitDepthDepthTextureH;
-  private static int mBlitDepthDepthH;
-
-  /// NORMAL PROGRAM /////
-  private static DistortedProgram mNormalProgram;
-  private static int mNormalMVPMatrixH;
-  /// END PROGRAMS //////
-
-  private static long mNextID =0;
-  private long mID;
-
-  private EffectQueueMatrix mM;
-  private EffectQueueFragment mF;
-  private EffectQueueVertex mV;
-
-  private boolean matrixCloned, vertexCloned, fragmentCloned;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void createProgram(Resources resources)
-  throws FragmentCompilationException,VertexCompilationException,VertexUniformsException,FragmentUniformsException,LinkingException
-    {
-    // MAIN PROGRAM ////////////////////////////////////
-    final InputStream mainVertStream = resources.openRawResource(R.raw.main_vertex_shader);
-    final InputStream mainFragStream = resources.openRawResource(R.raw.main_fragment_shader);
-
-    String mainVertHeader= Distorted.GLSL_VERSION;
-    String mainFragHeader= Distorted.GLSL_VERSION;
-
-    EffectNames name;
-    EffectTypes type;
-    boolean foundF = false;
-    boolean foundV = false;
-
-    for(int i=0; i<mEffectEnabled.length; i++)
-      {
-      if( mEffectEnabled[i] )
-        {
-        name = EffectNames.getName(i);
-        type = EffectNames.getType(i);
-
-        if( type == EffectTypes.VERTEX )
-          {
-          mainVertHeader += ("#define "+name.name()+" "+name.ordinal()+"\n");
-          foundV = true;
-          }
-        else if( type == EffectTypes.FRAGMENT )
-          {
-          mainFragHeader += ("#define "+name.name()+" "+name.ordinal()+"\n");
-          foundF = true;
-          }
-        }
-      }
-
-    mainVertHeader += ("#define NUM_VERTEX "   + ( foundV ? getMaxVertex()   : 0 ) + "\n");
-    mainFragHeader += ("#define NUM_FRAGMENT " + ( foundF ? getMaxFragment() : 0 ) + "\n");
-
-    //android.util.Log.e("Effects", "vertHeader= "+mainVertHeader);
-    //android.util.Log.e("Effects", "fragHeader= "+mainFragHeader);
-
-    String[] feedback = { "v_Position", "v_endPosition" };
-
-    mMainProgram = new DistortedProgram(mainVertStream,mainFragStream, mainVertHeader, mainFragHeader, Distorted.GLSL, feedback);
-
-    int mainProgramH = mMainProgram.getProgramHandle();
-    EffectQueueFragment.getUniforms(mainProgramH);
-    EffectQueueVertex.getUniforms(mainProgramH);
-    EffectQueueMatrix.getUniforms(mainProgramH);
-    mMainTextureH= GLES30.glGetUniformLocation( mainProgramH, "u_Texture");
-
-    // BLIT PROGRAM ////////////////////////////////////
-    final InputStream blitVertStream = resources.openRawResource(R.raw.blit_vertex_shader);
-    final InputStream blitFragStream = resources.openRawResource(R.raw.blit_fragment_shader);
-
-    String blitVertHeader= (Distorted.GLSL_VERSION + "#define NUM_VERTEX 0\n"  );
-    String blitFragHeader= (Distorted.GLSL_VERSION + "#define NUM_FRAGMENT 0\n");
-
-    try
-      {
-      mBlitProgram = new DistortedProgram(blitVertStream,blitFragStream,blitVertHeader,blitFragHeader, Distorted.GLSL);
-      }
-    catch(Exception e)
-      {
-      android.util.Log.e("EFFECTS", "exception trying to compile BLIT program: "+e.getMessage());
-      throw new RuntimeException(e.getMessage());
-      }
-
-    int blitProgramH = mBlitProgram.getProgramHandle();
-    mBlitTextureH  = GLES30.glGetUniformLocation( blitProgramH, "u_Texture");
-    mBlitDepthH    = GLES30.glGetUniformLocation( blitProgramH, "u_Depth");
-
-    // BLIT DEPTH PROGRAM ////////////////////////////////////
-    final InputStream blitDepthVertStream = resources.openRawResource(R.raw.blit_depth_vertex_shader);
-    final InputStream blitDepthFragStream = resources.openRawResource(R.raw.blit_depth_fragment_shader);
-
-    try
-      {
-      mBlitDepthProgram = new DistortedProgram(blitDepthVertStream,blitDepthFragStream,blitVertHeader,blitFragHeader, Distorted.GLSL);
-      }
-    catch(Exception e)
-      {
-      android.util.Log.e("EFFECTS", "exception trying to compile BLIT DEPTH program: "+e.getMessage());
-      throw new RuntimeException(e.getMessage());
-      }
-
-    int blitDepthProgramH   = mBlitDepthProgram.getProgramHandle();
-    mBlitDepthTextureH      = GLES30.glGetUniformLocation( blitDepthProgramH, "u_Texture");
-    mBlitDepthDepthTextureH = GLES30.glGetUniformLocation( blitDepthProgramH, "u_DepthTexture");
-    mBlitDepthDepthH        = GLES30.glGetUniformLocation( blitDepthProgramH, "u_Depth");
-
-    // NORMAL PROGRAM //////////////////////////////////////
-    final InputStream normalVertexStream   = resources.openRawResource(R.raw.normal_vertex_shader);
-    final InputStream normalFragmentStream = resources.openRawResource(R.raw.normal_fragment_shader);
-
-    try
-      {
-      mNormalProgram = new DistortedProgram(normalVertexStream,normalFragmentStream, Distorted.GLSL_VERSION, Distorted.GLSL_VERSION, Distorted.GLSL);
-      }
-    catch(Exception e)
-      {
-      android.util.Log.e("EFFECTS", "exception trying to compile NORMAL program: "+e.getMessage());
-      throw new RuntimeException(e.getMessage());
-      }
-
-    int normalProgramH = mNormalProgram.getProgramHandle();
-    mNormalMVPMatrixH  = GLES30.glGetUniformLocation( normalProgramH, "u_MVPMatrix");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void initializeEffectLists(DistortedEffects d, int flags)
-    {
-    if( (flags & Distorted.CLONE_MATRIX) != 0 )
-      {
-      mM = d.mM;
-      matrixCloned = true;
-      }
-    else
-      {
-      mM = new EffectQueueMatrix(mID);
-      matrixCloned = false;
-      }
-    
-    if( (flags & Distorted.CLONE_VERTEX) != 0 )
-      {
-      mV = d.mV;
-      vertexCloned = true;
-      }
-    else
-      {
-      mV = new EffectQueueVertex(mID);
-      vertexCloned = false;
-      }
-    
-    if( (flags & Distorted.CLONE_FRAGMENT) != 0 )
-      {
-      mF = d.mF;
-      fragmentCloned = true;
-      }
-    else
-      {
-      mF = new EffectQueueFragment(mID);
-      fragmentCloned = false;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void displayNormals(MeshObject mesh)
-    {
-    GLES30.glBindBufferBase(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, 0, mesh.mAttTFO[0]);
-    GLES30.glBeginTransformFeedback( GLES30.GL_POINTS);
-    DistortedRenderState.switchOffDrawing();
-    GLES30.glDrawArrays( GLES30.GL_POINTS, 0, mesh.numVertices);
-    DistortedRenderState.restoreDrawing();
-    GLES30.glEndTransformFeedback();
-    GLES30.glBindBufferBase(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
-
-    mNormalProgram.useProgram();
-    GLES30.glUniformMatrix4fv(mNormalMVPMatrixH, 1, false, mM.getMVP() , 0);
-    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mesh.mAttTFO[0]);
-    GLES30.glVertexAttribPointer(mNormalProgram.mAttribute[0], MeshObject.POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, 0);
-    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
-    GLES30.glLineWidth(8.0f);
-    GLES30.glDrawArrays(GLES30.GL_LINES, 0, 2*mesh.numVertices);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void drawPriv(float halfW, float halfH, MeshObject mesh, DistortedOutputSurface surface, long currTime, float marginInPixels)
-    {
-    mM.compute(currTime);
-    mV.compute(currTime);
-    mF.compute(currTime);
-
-    float halfZ = halfW*mesh.zFactor;
-
-    GLES30.glViewport(0, 0, surface.mWidth, surface.mHeight );
-
-    mMainProgram.useProgram();
-    GLES30.glUniform1i(mMainTextureH, 0);
-
-    if( Distorted.GLSL >= 300 )
-      {
-      GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mesh.mAttVBO[0]);
-      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[0], MeshObject.POS_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, MeshObject.OFFSET0);
-      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[1], MeshObject.NOR_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, MeshObject.OFFSET1);
-      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[2], MeshObject.TEX_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, MeshObject.OFFSET2);
-      GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
-      }
-    else
-      {
-      mesh.mVertAttribs.position(0);
-      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[0], MeshObject.POS_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, mesh.mVertAttribs);
-      mesh.mVertAttribs.position(MeshObject.POS_DATA_SIZE);
-      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[1], MeshObject.NOR_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, mesh.mVertAttribs);
-      mesh.mVertAttribs.position(MeshObject.POS_DATA_SIZE+MeshObject.NOR_DATA_SIZE);
-      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[2], MeshObject.TEX_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, mesh.mVertAttribs);
-      }
-
-    mM.send(surface,halfW,halfH,halfZ,marginInPixels);
-    mV.send(halfW,halfH,halfZ);
-    mF.send(halfW,halfH);
-
-    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mesh.numVertices);
-
-    if( mesh.mShowNormals ) displayNormals(mesh);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  static void blitPriv(DistortedOutputSurface surface)
-    {
-    mBlitProgram.useProgram();
-
-    GLES30.glViewport(0, 0, surface.mWidth, surface.mHeight );
-    GLES30.glUniform1i(mBlitTextureH, 0);
-    GLES30.glUniform1f( mBlitDepthH , 1.0f-surface.mNear);
-    GLES30.glVertexAttribPointer(mBlitProgram.mAttribute[0], 2, GLES30.GL_FLOAT, false, 0, mQuadPositions);
-    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void blitDepthPriv(DistortedOutputSurface surface)
-    {
-    mBlitDepthProgram.useProgram();
-
-    GLES30.glViewport(0, 0, surface.mWidth, surface.mHeight );
-    GLES30.glUniform1i(mBlitDepthTextureH, 0);
-    GLES30.glUniform1i(mBlitDepthDepthTextureH, 1);
-    GLES30.glUniform1f( mBlitDepthDepthH , 1.0f-surface.mNear);
-    GLES30.glVertexAttribPointer(mBlitDepthProgram.mAttribute[0], 2, GLES30.GL_FLOAT, false, 0, mQuadPositions);
-    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  private void releasePriv()
-    {
-    if( !matrixCloned   ) mM.abortAll(false);
-    if( !vertexCloned   ) mV.abortAll(false);
-    if( !fragmentCloned ) mF.abortAll(false);
-
-    mM = null;
-    mV = null;
-    mF = null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void onDestroy()
-    {
-    mNextID = 0;
-
-    int len = EffectNames.size();
-
-    for(int i=0; i<len; i++)
-      {
-      mEffectEnabled[i] = false;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create empty effect queue.
- */
-  public DistortedEffects()
-    {
-    mID = ++mNextID;
-    initializeEffectLists(this,0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Copy constructor.
- * <p>
- * Whatever we do not clone gets created just like in the default constructor.
- *
- * @param dc    Source object to create our object from
- * @param flags A bitmask of values specifying what to copy.
- *              For example, CLONE_VERTEX | CLONE_MATRIX.
- */
-  public DistortedEffects(DistortedEffects dc, int flags)
-    {
-    mID = ++mNextID;
-    initializeEffectLists(dc,flags);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Releases all resources. After this call, the queue should not be used anymore.
- */
-  @SuppressWarnings("unused")
-  public synchronized void delete()
-    {
-    releasePriv();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns unique ID of this instance.
- *
- * @return ID of the object.
- */
-  public long getID()
-      {
-      return mID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Adds the calling class to the list of Listeners that get notified each time some event happens 
- * to one of the Effects in those queues. Nothing will happen if 'el' is already in the list.
- * 
- * @param el A class implementing the EffectListener interface that wants to get notifications.
- */
-  @SuppressWarnings("unused")
-  public void registerForMessages(EffectListener el)
-    {
-    mV.registerForMessages(el);
-    mF.registerForMessages(el);
-    mM.registerForMessages(el);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes the calling class from the list of Listeners.
- * 
- * @param el A class implementing the EffectListener interface that no longer wants to get notifications.
- */
-  @SuppressWarnings("unused")
-  public void deregisterForMessages(EffectListener el)
-    {
-    mV.deregisterForMessages(el);
-    mF.deregisterForMessages(el);
-    mM.deregisterForMessages(el);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Aborts all Effects.
- * @return Number of effects aborted.
- */
-  public int abortAllEffects()
-    {
-    return mM.abortAll(true) + mV.abortAll(true) + mF.abortAll(true);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Aborts all Effects of a given type, for example all MATRIX Effects.
- * 
- * @param type one of the constants defined in {@link EffectTypes}
- * @return Number of effects aborted.
- */
-  public int abortEffects(EffectTypes type)
-    {
-    switch(type)
-      {
-      case MATRIX     : return mM.abortAll(true);
-      case VERTEX     : return mV.abortAll(true);
-      case FRAGMENT   : return mF.abortAll(true);
-      default         : return 0;
-      }
-    }
-    
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Aborts a single Effect.
- * 
- * @param id ID of the Effect we want to abort.
- * @return number of Effects aborted. Always either 0 or 1.
- */
-  public int abortEffect(long id)
-    {
-    int type = (int)(id&EffectTypes.MASK);
-
-    if( type==EffectTypes.MATRIX.type      ) return mM.removeByID(id>>EffectTypes.LENGTH);
-    if( type==EffectTypes.VERTEX.type      ) return mV.removeByID(id>>EffectTypes.LENGTH);
-    if( type==EffectTypes.FRAGMENT.type    ) return mF.removeByID(id>>EffectTypes.LENGTH);
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Abort all Effects of a given name, for example all rotations.
- * 
- * @param name one of the constants defined in {@link EffectNames}
- * @return number of Effects aborted.
- */
-  public int abortEffects(EffectNames name)
-    {
-    switch(name.getType())
-      {
-      case MATRIX     : return mM.removeByType(name);
-      case VERTEX     : return mV.removeByType(name);
-      case FRAGMENT   : return mF.removeByType(name);
-      default         : return 0;
-      }
-    }
-    
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Print some info about a given Effect to Android's standard out. Used for debugging only.
- * 
- * @param id Effect ID we want to print info about
- * @return <code>true</code> if a single Effect of type effectType has been found.
- */
-  @SuppressWarnings("unused")
-  public boolean printEffect(long id)
-    {
-    int type = (int)(id&EffectTypes.MASK);
-
-    if( type==EffectTypes.MATRIX.type  )  return mM.printByID(id>>EffectTypes.LENGTH);
-    if( type==EffectTypes.VERTEX.type  )  return mV.printByID(id>>EffectTypes.LENGTH);
-    if( type==EffectTypes.FRAGMENT.type)  return mF.printByID(id>>EffectTypes.LENGTH);
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Enables a given Effect.
- * <p>
- * By default, all effects are disabled. One has to explicitly enable each effect one intends to use.
- * This needs to be called BEFORE shaders get compiled, i.e. before the call to Distorted.onCreate().
- * The point: by enabling only the effects we need, we can optimize the shaders.
- *
- * @param name Name of the Effect to enable.
- */
-  public static void enableEffect(EffectNames name)
-    {
-    mEffectEnabled[name.ordinal()] = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns the maximum number of Matrix effects.
- *
- * @return The maximum number of Matrix effects
- */
-  @SuppressWarnings("unused")
-  public static int getMaxMatrix()
-    {
-    return EffectQueue.getMax(EffectTypes.MATRIX.ordinal());
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns the maximum number of Vertex effects.
- *
- * @return The maximum number of Vertex effects
- */
-  @SuppressWarnings("unused")
-  public static int getMaxVertex()
-    {
-    return EffectQueue.getMax(EffectTypes.VERTEX.ordinal());
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns the maximum number of Fragment effects.
- *
- * @return The maximum number of Fragment effects
- */
-  @SuppressWarnings("unused")
-  public static int getMaxFragment()
-    {
-    return EffectQueue.getMax(EffectTypes.FRAGMENT.ordinal());
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Sets the maximum number of Matrix effects that can be stored in a single EffectQueue at one time.
- * This can fail if:
- * <ul>
- * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
- * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
- *     before the Vertex Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
- *     time only decreasing the value of 'max' is permitted.
- * <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
- * </ul>
- *
- * @param max new maximum number of simultaneous Matrix Effects. Has to be a non-negative number not greater
- *            than Byte.MAX_VALUE
- * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
- */
-  @SuppressWarnings("unused")
-  public static boolean setMaxMatrix(int max)
-    {
-    return EffectQueue.setMax(EffectTypes.MATRIX.ordinal(),max);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Sets the maximum number of Vertex effects that can be stored in a single EffectQueue at one time.
- * This can fail if:
- * <ul>
- * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
- * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
- *     before the Vertex Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
- *     time only decreasing the value of 'max' is permitted.
-* <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
- * </ul>
- *
- * @param max new maximum number of simultaneous Vertex Effects. Has to be a non-negative number not greater
- *            than Byte.MAX_VALUE
- * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
- */
-  @SuppressWarnings("unused")
-  public static boolean setMaxVertex(int max)
-    {
-    return EffectQueue.setMax(EffectTypes.VERTEX.ordinal(),max);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Sets the maximum number of Fragment effects that can be stored in a single EffectQueue at one time.
- * This can fail if:
- * <ul>
- * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
- * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
- *     before the Fragment Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
- *     time only decreasing the value of 'max' is permitted.
- * <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
- * </ul>
- *
- * @param max new maximum number of simultaneous Fragment Effects. Has to be a non-negative number not greater
- *            than Byte.MAX_VALUE
- * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
- */
-  @SuppressWarnings("unused")
-  public static boolean setMaxFragment(int max)
-    {
-    return EffectQueue.setMax(EffectTypes.FRAGMENT.ordinal(),max);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Individual effect functions.
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Matrix-based effects
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Moves the Object by a (possibly changing in time) vector.
- * 
- * @param vector 3-dimensional Data which at any given time will return a Static3D
- *               representing the current coordinates of the vector we want to move the Object with.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long move(Data3D vector)
-    {   
-    return mM.add(EffectNames.MOVE,vector);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Scales the Object by (possibly changing in time) 3D scale factors.
- * 
- * @param scale 3-dimensional Data which at any given time returns a Static3D
- *              representing the current x- , y- and z- scale factors.
- * @return      ID of the effect added, or -1 if we failed to add one.
- */
-  public long scale(Data3D scale)
-    {   
-    return mM.add(EffectNames.SCALE,scale);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Scales the Object by one uniform, constant factor in all 3 dimensions. Convenience function.
- *
- * @param scale The factor to scale all 3 dimensions with.
- * @return      ID of the effect added, or -1 if we failed to add one.
- */
-  public long scale(float scale)
-    {
-    return mM.add(EffectNames.SCALE, new Static3D(scale,scale,scale));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Rotates the Object by 'angle' degrees around the center.
- * Static axis of rotation is given by the last parameter.
- *
- * @param angle  Angle that we want to rotate the Object to. Unit: degrees
- * @param axis   Axis of rotation
- * @param center Coordinates of the Point we are rotating around.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long rotate(Data1D angle, Static3D axis, Data3D center )
-    {   
-    return mM.add(EffectNames.ROTATE, angle, axis, center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Rotates the Object by 'angle' degrees around the center.
- * Here both angle and axis can dynamically change.
- *
- * @param angleaxis Combined 4-tuple representing the (angle,axisX,axisY,axisZ).
- * @param center    Coordinates of the Point we are rotating around.
- * @return          ID of the effect added, or -1 if we failed to add one.
- */
-  public long rotate(Data4D angleaxis, Data3D center)
-    {
-    return mM.add(EffectNames.ROTATE, angleaxis, center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Rotates the Object by quaternion.
- *
- * @param quaternion The quaternion describing the rotation.
- * @param center     Coordinates of the Point we are rotating around.
- * @return           ID of the effect added, or -1 if we failed to add one.
- */
-  public long quaternion(Data4D quaternion, Data3D center )
-    {
-    return mM.add(EffectNames.QUATERNION,quaternion,center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Shears the Object.
- *
- * @param shear   The 3-tuple of shear factors. The first controls level
- *                of shearing in the X-axis, second - Y-axis and the third -
- *                Z-axis. Each is the tangens of the shear angle, i.e 0 -
- *                no shear, 1 - shear by 45 degrees (tan(45deg)=1) etc.
- * @param center  Center of shearing, i.e. the point which stays unmoved.
- * @return        ID of the effect added, or -1 if we failed to add one.
- */
-  public long shear(Data3D shear, Data3D center)
-    {
-    return mM.add(EffectNames.SHEAR, shear, center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Fragment-based effects  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes a certain sub-region of the Object smoothly change all three of its RGB components.
- *        
- * @param blend  1-dimensional Data that returns the level of blend a given pixel will be
- *               mixed with the next parameter 'color': pixel = (1-level)*pixel + level*color.
- *               Valid range: <0,1>
- * @param color  Color to mix. (1,0,0) is RED.
- * @param region Region this Effect is limited to.
- * @param smooth If true, the level of 'blend' will smoothly fade out towards the edges of the region.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long chroma(Data1D blend, Data3D color, Data4D region, boolean smooth)
-    {
-    return mF.add( smooth? EffectNames.SMOOTH_CHROMA:EffectNames.CHROMA, blend, color, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes the whole Object smoothly change all three of its RGB components.
- *
- * @param blend  1-dimensional Data that returns the level of blend a given pixel will be
- *               mixed with the next parameter 'color': pixel = (1-level)*pixel + level*color.
- *               Valid range: <0,1>
- * @param color  Color to mix. (1,0,0) is RED.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long chroma(Data1D blend, Data3D color)
-    {
-    return mF.add(EffectNames.CHROMA, blend, color);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes a certain sub-region of the Object smoothly change its transparency level.
- *        
- * @param alpha  1-dimensional Data that returns the level of transparency we want to have at any given
- *               moment: pixel.a *= alpha.
- *               Valid range: <0,1>
- * @param region Region this Effect is limited to. 
- * @param smooth If true, the level of 'alpha' will smoothly fade out towards the edges of the region.
- * @return       ID of the effect added, or -1 if we failed to add one. 
- */
-  public long alpha(Data1D alpha, Data4D region, boolean smooth)
-    {
-    return mF.add( smooth? EffectNames.SMOOTH_ALPHA:EffectNames.ALPHA, alpha, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes the whole Object smoothly change its transparency level.
- *
- * @param alpha  1-dimensional Data that returns the level of transparency we want to have at any
- *               given moment: pixel.a *= alpha.
- *               Valid range: <0,1>
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long alpha(Data1D alpha)
-    {
-    return mF.add(EffectNames.ALPHA, alpha);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes a certain sub-region of the Object smoothly change its brightness level.
- *        
- * @param brightness 1-dimensional Data that returns the level of brightness we want to have
- *                   at any given moment. Valid range: <0,infinity)
- * @param region     Region this Effect is limited to.
- * @param smooth     If true, the level of 'brightness' will smoothly fade out towards the edges of the region.
- * @return           ID of the effect added, or -1 if we failed to add one.
- */
-  public long brightness(Data1D brightness, Data4D region, boolean smooth)
-    {
-    return mF.add( smooth ? EffectNames.SMOOTH_BRIGHTNESS: EffectNames.BRIGHTNESS, brightness, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes the whole Object smoothly change its brightness level.
- *
- * @param brightness 1-dimensional Data that returns the level of brightness we want to have
- *                   at any given moment. Valid range: <0,infinity)
- * @return           ID of the effect added, or -1 if we failed to add one.
- */
-  public long brightness(Data1D brightness)
-    {
-    return mF.add(EffectNames.BRIGHTNESS, brightness);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes a certain sub-region of the Object smoothly change its contrast level.
- *        
- * @param contrast 1-dimensional Data that returns the level of contrast we want to have
- *                 at any given moment. Valid range: <0,infinity)
- * @param region   Region this Effect is limited to.
- * @param smooth   If true, the level of 'contrast' will smoothly fade out towards the edges of the region.
- * @return         ID of the effect added, or -1 if we failed to add one.
- */
-  public long contrast(Data1D contrast, Data4D region, boolean smooth)
-    {
-    return mF.add( smooth ? EffectNames.SMOOTH_CONTRAST:EffectNames.CONTRAST, contrast, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes the whole Object smoothly change its contrast level.
- *
- * @param contrast 1-dimensional Data that returns the level of contrast we want to have
- *                 at any given moment. Valid range: <0,infinity)
- * @return         ID of the effect added, or -1 if we failed to add one.
- */
-  public long contrast(Data1D contrast)
-    {
-    return mF.add(EffectNames.CONTRAST, contrast);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes a certain sub-region of the Object smoothly change its saturation level.
- *        
- * @param saturation 1-dimensional Data that returns the level of saturation we want to have
- *                   at any given moment. Valid range: <0,infinity)
- * @param region     Region this Effect is limited to.
- * @param smooth     If true, the level of 'saturation' will smoothly fade out towards the edges of the region.
- * @return           ID of the effect added, or -1 if we failed to add one.
- */
-  public long saturation(Data1D saturation, Data4D region, boolean smooth)
-    {
-    return mF.add( smooth ? EffectNames.SMOOTH_SATURATION:EffectNames.SATURATION, saturation, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Makes the whole Object smoothly change its saturation level.
- *
- * @param saturation 1-dimensional Data that returns the level of saturation we want to have
- *                   at any given moment. Valid range: <0,infinity)
- * @return           ID of the effect added, or -1 if we failed to add one.
- */
-  public long saturation(Data1D saturation)
-    {
-    return mF.add(EffectNames.SATURATION, saturation);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Vertex-based effects  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Distort a (possibly changing in time) part of the Object by a (possibly changing in time) vector of force.
- *
- * @param vector 3-dimensional Vector which represents the force the Center of the Effect is
- *               currently being dragged with.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @param region Region that masks the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long distort(Data3D vector, Data3D center, Data4D region)
-    {  
-    return mV.add(EffectNames.DISTORT, vector, center, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Distort the whole Object by a (possibly changing in time) vector of force.
- *
- * @param vector 3-dimensional Vector which represents the force the Center of the Effect is
- *               currently being dragged with.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long distort(Data3D vector, Data3D center)
-    {
-    return mV.add(EffectNames.DISTORT, vector, center, null);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Deform the shape of the whole Object with a (possibly changing in time) vector of force applied to
- * a (possibly changing in time) point on the Object.
- *
- * @param vector Vector of force that deforms the shape of the whole Object.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @param region Region that masks the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long deform(Data3D vector, Data3D center, Data4D region)
-    {
-    return mV.add(EffectNames.DEFORM, vector, center, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Deform the shape of the whole Object with a (possibly changing in time) vector of force applied to
- * a (possibly changing in time) point on the Object.
- *     
- * @param vector Vector of force that deforms the shape of the whole Object.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long deform(Data3D vector, Data3D center)
-    {  
-    return mV.add(EffectNames.DEFORM, vector, center, null);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////  
-/**
- * Pull all points around the center of the Effect towards the center (if degree>=1) or push them
- * away from the center (degree<=1)
- *
- * @param sink   The current degree of the Effect.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @param region Region that masks the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long sink(Data1D sink, Data3D center, Data4D region)
-    {
-    return mV.add(EffectNames.SINK, sink, center, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Pull all points around the center of the Effect towards the center (if degree>=1) or push them
- * away from the center (degree<=1)
- *
- * @param sink   The current degree of the Effect.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long sink(Data1D sink, Data3D center)
-    {
-    return mV.add(EffectNames.SINK, sink, center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Pull all points around the center of the Effect towards a line passing through the center
- * (that's if degree>=1) or push them away from the line (degree<=1)
- *
- * @param pinch  The current degree of the Effect + angle the line forms with X-axis
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @param region Region that masks the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long pinch(Data2D pinch, Data3D center, Data4D region)
-    {
-    return mV.add(EffectNames.PINCH, pinch, center, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Pull all points around the center of the Effect towards a line passing through the center
- * (that's if degree>=1) or push them away from the line (degree<=1)
- *
- * @param pinch  The current degree of the Effect + angle the line forms with X-axis
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long pinch(Data2D pinch, Data3D center)
-    {
-    return mV.add(EffectNames.PINCH, pinch, center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////  
-/**
- * Rotate part of the Object around the Center of the Effect by a certain angle.
- *
- * @param swirl  The angle of Swirl (in degrees). Positive values swirl clockwise.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @param region Region that masks the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long swirl(Data1D swirl, Data3D center, Data4D region)
-    {    
-    return mV.add(EffectNames.SWIRL, swirl, center, region);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Rotate the whole Object around the Center of the Effect by a certain angle.
- *
- * @param swirl  The angle of Swirl (in degrees). Positive values swirl clockwise.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long swirl(Data1D swirl, Data3D center)
-    {
-    return mV.add(EffectNames.SWIRL, swirl, center);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Directional, sinusoidal wave effect.
- *
- * @param wave   A 5-dimensional data structure describing the wave: first member is the amplitude,
- *               second is the wave length, third is the phase (i.e. when phase = PI/2, the sine
- *               wave at the center does not start from sin(0), but from sin(PI/2) ) and the next two
- *               describe the 'direction' of the wave.
- *               <p>
- *               Wave direction is defined to be a 3D vector of length 1. To define such vectors, we
- *               need 2 floats: thus the third member is the angle Alpha (in degrees) which the vector
- *               forms with the XY-plane, and the fourth is the angle Beta (again in degrees) which
- *               the projection of the vector to the XY-plane forms with the Y-axis (counterclockwise).
- *               <p>
- *               <p>
- *               Example1: if Alpha = 90, Beta = 90, (then V=(0,0,1) ) and the wave acts 'vertically'
- *               in the X-direction, i.e. cross-sections of the resulting surface with the XZ-plane
- *               will be sine shapes.
- *               <p>
- *               Example2: if Alpha = 90, Beta = 0, the again V=(0,0,1) and the wave is 'vertical',
- *               but this time it waves in the Y-direction, i.e. cross sections of the surface and the
- *               YZ-plane with be sine shapes.
- *               <p>
- *               Example3: if Alpha = 0 and Beta = 45, then V=(sqrt(2)/2, -sqrt(2)/2, 0) and the wave
- *               is entirely 'horizontal' and moves point (x,y,0) in direction V by whatever is the
- *               value if sin at this point.
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long wave(Data5D wave, Data3D center)
-    {
-    return mV.add(EffectNames.WAVE, wave, center, null);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Directional, sinusoidal wave effect.
- *
- * @param wave   see {@link DistortedEffects#wave(Data5D,Data3D)}
- * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
- * @param region Region that masks the Effect.
- * @return       ID of the effect added, or -1 if we failed to add one.
- */
-  public long wave(Data5D wave, Data3D center, Data4D region)
-    {
-    return mV.add(EffectNames.WAVE, wave, center, region);
-    }
-  }
diff --git a/src/main/java/org/distorted/library/DistortedEffectsPostprocess.java b/src/main/java/org/distorted/library/DistortedEffectsPostprocess.java
deleted file mode 100644
index 9368bff..0000000
--- a/src/main/java/org/distorted/library/DistortedEffectsPostprocess.java
+++ /dev/null
@@ -1,391 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import org.distorted.library.effect.EffectQueue;
-import org.distorted.library.effect.EffectQueuePostprocess;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.type.Data1D;
-import org.distorted.library.type.Data4D;
-
-import java.util.ArrayList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-/**
- * Class containing the queue of postprocessing effects.
- * <p>
- * This better be separate from the main DistortedEffects class, because we want to be able to apply
- * the same postprocessing effects (i.e. not only the same effects, but the very same instance of this
- * object) to several Children of a Node. Reason for that: if several children have the same Object,
- * it is easy to group them into 'Buckets' where each bucket has the same postprocessing effects and
- * is therefore rendered to the same buffer, which is then postprocessed in one go.
- * <p>
- * The queue holds actual effects to be applied to a given bucket of several (DistortedTexture,MeshObject) combos.
- */
-public class DistortedEffectsPostprocess implements DistortedSlave
-  {
-  private static final int MIPMAP = 0;
-
-  private static long mNextID =0;
-  private long mID;
-
-  private EffectQueuePostprocess mP;
-
-  private boolean postprocessCloned;
-
-  private class Job
-    {
-    int type;
-    int level;
-
-    Job(int t, int l)
-      {
-      type = t;
-      level= l;
-      }
-    }
-
-  private ArrayList<Job> mJobs = new ArrayList<>();
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void initializeEffectLists(DistortedEffectsPostprocess d, int flags)
-    {
-    if( (flags & Distorted.CLONE_POSTPROCESS) != 0 )
-      {
-      mP = d.mP;
-      postprocessCloned = true;
-      }
-    else
-      {
-      mP = new EffectQueuePostprocess(mID);
-      postprocessCloned = false;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int postprocess(long time, DistortedOutputSurface surface)
-    {
-    return mP.postprocess(time,surface);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  long getBucket()
-    {
-    return mP.mNumEffects==0 ? 0: mID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void releasePriv()
-    {
-    if( !postprocessCloned) mP.abortAll(false);
-
-    mP = null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getQuality()
-    {
-    return mP.mQualityLevel;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getHalo()
-    {
-    return mP.getHalo();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void onDestroy()
-    {
-    mNextID = 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create empty effect queue.
- */
-  public DistortedEffectsPostprocess()
-    {
-    mID = ++mNextID;
-    initializeEffectLists(this,0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Copy constructor.
- * <p>
- * Whatever we do not clone gets created just like in the default constructor.
- *
- * @param dc    Source object to create our object from
- * @param flags A bitmask of values specifying what to copy.
- *              Currently the only values possible are CLONE_NOTHING or CLONE_POSTPROCESS.
- */
-  public DistortedEffectsPostprocess(DistortedEffectsPostprocess dc, int flags)
-    {
-    mID = ++mNextID;
-    initializeEffectLists(dc,flags);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * This is not really part of the public API. Has to be public only because it is a part of the
- * DistortedSlave interface, which should really be a class that we extend here instead but
- * Java has no multiple inheritance.
- *
- * @y.exclude
- */
-  public void doWork()
-    {
-    int num = mJobs.size();
-    Job job;
-
-    for(int i=0; i<num; i++)
-      {
-      job = mJobs.remove(0);
-
-      switch(job.type)
-        {
-        case MIPMAP: int level = job.level;
-                     mP.mQualityLevel = level;
-                     mP.mQualityScale = 1.0f;
-                     for(int j=0; j<level; j++) mP.mQualityScale*=EffectQuality.MULTIPLIER;
-                     break;
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Releases all resources. After this call, the queue should not be used anymore.
- */
-  @SuppressWarnings("unused")
-  public synchronized void delete()
-    {
-    releasePriv();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns unique ID of this instance.
- *
- * @return ID of the object.
- */
-  @SuppressWarnings("unused")
-  public long getID()
-      {
-      return mID;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Adds the calling class to the list of Listeners that get notified each time some event happens 
- * to one of the Effects in the queues. Nothing will happen if 'el' is already in the list.
- * 
- * @param el A class implementing the EffectListener interface that wants to get notifications.
- */
-  @SuppressWarnings("unused")
-  public void registerForMessages(EffectListener el)
-    {
-    mP.registerForMessages(el);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes the calling class from the list of Listeners.
- * 
- * @param el A class implementing the EffectListener interface that no longer wants to get notifications.
- */
-  @SuppressWarnings("unused")
-  public void deregisterForMessages(EffectListener el)
-    {
-    mP.deregisterForMessages(el);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Aborts all Effects.
- * @return Number of effects aborted.
- */
-  @SuppressWarnings("unused")
-  public int abortAllEffects()
-      {
-      return mP.abortAll(true);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Aborts all Effects of a given type (currently only POSTPROCESSING Effects).
- * 
- * @param type one of the constants defined in {@link EffectTypes}
- * @return Number of effects aborted.
- */
-  @SuppressWarnings("unused")
-  public int abortEffects(EffectTypes type)
-    {
-    switch(type)
-      {
-      case POSTPROCESS: return mP.abortAll(true);
-      default         : return 0;
-      }
-    }
-    
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Aborts a single Effect.
- * 
- * @param id ID of the Effect we want to abort.
- * @return number of Effects aborted. Always either 0 or 1.
- */
-  @SuppressWarnings("unused")
-  public int abortEffect(long id)
-    {
-    int type = (int)(id&EffectTypes.MASK);
-
-    if( type==EffectTypes.POSTPROCESS.type ) return mP.removeByID(id>>EffectTypes.LENGTH);
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Abort all Effects of a given name, for example all blurs.
- * 
- * @param name one of the constants defined in {@link EffectNames}
- * @return number of Effects aborted.
- */
-  @SuppressWarnings("unused")
-  public int abortEffects(EffectNames name)
-    {
-    switch(name.getType())
-      {
-      case POSTPROCESS: return mP.removeByType(name);
-      default         : return 0;
-      }
-    }
-    
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Print some info about a given Effect to Android's standard out. Used for debugging only.
- * 
- * @param id Effect ID we want to print info about
- * @return <code>true</code> if a single Effect of type effectType has been found.
- */
-  @SuppressWarnings("unused")
-  public boolean printEffect(long id)
-    {
-    int type = (int)(id&EffectTypes.MASK);
-
-    if( type==EffectTypes.POSTPROCESS.type )  return mP.printByID(id>>EffectTypes.LENGTH);
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns the maximum number of Postprocess effects.
- *
- * @return The maximum number of Postprocess effects
- */
-  @SuppressWarnings("unused")
-  public static int getMaxPostprocess()
-    {
-    return EffectQueue.getMax(EffectTypes.POSTPROCESS.ordinal());
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Sets the maximum number of Postprocess effects that can be stored in a single EffectQueue at one time.
- * This can fail if:
- * <ul>
- * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
- * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
- *     before the Fragment Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
- *     time only decreasing the value of 'max' is permitted.
- * <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
- * </ul>
- *
- * @param max new maximum number of simultaneous Postprocess Effects. Has to be a non-negative number not greater
- *            than Byte.MAX_VALUE
- * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
- */
-  @SuppressWarnings("unused")
-  public static boolean setMaxPostprocess(int max)
-    {
-    return EffectQueue.setMax(EffectTypes.POSTPROCESS.ordinal(),max);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * The higher the quality, the better the effect will look like and the slower it will be.
- * <p>
- * This works by rendering into smaller and smaller intermediate buffers. Each step renders into a
- * buffer that's half the size of the previous one.
- * <p>
- * We cannot this during mid-render - thus, give it to the Master to assign us back this job on the
- * next render.
- */
-  public void setQuality(EffectQuality quality)
-    {
-    mJobs.add(new Job(MIPMAP,quality.level));
-    DistortedMaster.newSlave(this);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Individual effect functions.
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Postprocess-based effects
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Blur the object.
- *
- * @param radius The 'strength' if the effect, in pixels. 0 = no blur, 10 = when blurring a given pixel,
- *               take into account 10 pixels in each direction.
- * @return ID of the effect added, or -1 if we failed to add one.
- */
-  public long blur(Data1D radius)
-    {
-    return mP.add(EffectNames.BLUR, radius);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Make the object glow with a specific color and a halo of specific radius.
- *
- * @param radius The 'strength' if the effect, in pixels. 0 = no halo, 10 = halo of roughly 10 pixels
- *               around the whole object.
- * @param color  RGBA of the color with which to draw the glow.
- * @return ID of the effect added, or -1 if we failed to add one.
- */
-  public long glow(Data1D radius, Data4D color)
-    {
-    return mP.add(EffectNames.GLOW, radius, color);
-    }
-  }
diff --git a/src/main/java/org/distorted/library/DistortedFramebuffer.java b/src/main/java/org/distorted/library/DistortedFramebuffer.java
deleted file mode 100644
index 6d6dcd1..0000000
--- a/src/main/java/org/distorted/library/DistortedFramebuffer.java
+++ /dev/null
@@ -1,268 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import android.opengl.GLES30;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Class which represents a OpenGL Framebuffer object.
- * <p>
- * User is able to create offscreen FBOs and both a) render to them b) use their COLOR0 attachment as
- * an input texture. Attaching Depths and/or Stencils is also possible.
- */
-public class DistortedFramebuffer extends DistortedOutputSurface implements DistortedInputSurface
-  {
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void prepareDebug(long time) {}
-  void renderDebug(long time)  {}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Must be called from a thread holding OpenGL Context
-
-  void create()
-    {
-    if( mColorCreated==NOT_CREATED_YET )
-      {
-      GLES30.glGenTextures( mNumColors, mColorH, 0);
-      GLES30.glGenFramebuffers(1, mFBOH, 0);
-      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
-
-      for(int i=0; i<mNumColors; i++)
-        {
-        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[i]);
-        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT);
-        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT);
-        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
-        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
-        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mWidth, mHeight, 0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
-        }
-      GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mColorH[0], 0);
-      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
-      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);
-
-      mColorCreated = checkStatus("color");
-      }
-    if( mDepthStencilCreated==NOT_CREATED_YET ) // we need to create a new DEPTH or STENCIL attachment
-      {
-      GLES30.glGenTextures(1, mDepthStencilH, 0);
-      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mDepthStencilH[0]);
-      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT);
-      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT);
-      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
-      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_NEAREST);
-
-      if( mDepthStencil==DEPTH_NO_STENCIL )
-        {
-        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_DEPTH_COMPONENT, mWidth, mHeight, 0, GLES30.GL_DEPTH_COMPONENT, GLES30.GL_UNSIGNED_INT, null);
-        }
-      else if( mDepthStencil==BOTH_DEPTH_STENCIL )
-        {
-        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_DEPTH24_STENCIL8, mWidth, mHeight, 0, GLES30.GL_DEPTH_STENCIL, GLES30.GL_UNSIGNED_INT_24_8, null);
-        }
-
-      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
-      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
-
-      if( mDepthStencil==DEPTH_NO_STENCIL )
-        {
-        GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_DEPTH_ATTACHMENT, GLES30.GL_TEXTURE_2D, mDepthStencilH[0], 0);
-        }
-      else if( mDepthStencil==BOTH_DEPTH_STENCIL )
-        {
-        GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_DEPTH_STENCIL_ATTACHMENT, GLES30.GL_TEXTURE_2D, mDepthStencilH[0], 0);
-        }
-
-      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);
-
-      mDepthStencilCreated = checkStatus("depth");
-      }
-    if( mDepthStencilCreated==DONT_CREATE && mDepthStencilH[0]>0 ) // we need to detach and recreate the DEPTH attachment.
-      {
-      GLES30.glDeleteTextures(1, mDepthStencilH, 0);
-      mDepthStencilH[0]=0;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int checkStatus(String message)
-    {
-    int status = GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER);
-
-    if(status != GLES30.GL_FRAMEBUFFER_COMPLETE)
-      {
-      android.util.Log.e("DistortedFramebuffer", "FRAMEBUFFER INCOMPLETE, "+message+" error="+status);
-
-      GLES30.glDeleteTextures(1, mColorH, 0);
-      GLES30.glDeleteTextures(1, mDepthStencilH, 0);
-      GLES30.glDeleteFramebuffers(1, mFBOH, 0);
-      mFBOH[0]= 0;
-
-      return FAILED_TO_CREATE;
-      }
-
-    return CREATED;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Must be called from a thread holding OpenGL Context
-
-  void delete()
-    {
-    if( mColorH[0]>0 )
-      {
-      GLES30.glDeleteTextures(1, mColorH, 0);
-      mColorH[0] = 0;
-      mColorCreated = NOT_CREATED_YET;
-      }
-
-    if( mDepthStencilH[0]>0 )
-      {
-      GLES30.glDeleteTextures(1, mDepthStencilH, 0);
-      mDepthStencilH[0]=0;
-      mDepthStencilCreated = NOT_CREATED_YET;
-      }
-
-    GLES30.glDeleteFramebuffers(1, mFBOH, 0);
-    mFBOH[0] = 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// called from onDestroy(); mark OpenGL assets as 'not created'
-
-  void recreate()
-    {
-    if( mColorCreated!=DONT_CREATE )
-      {
-      mColorCreated = NOT_CREATED_YET;
-      mColorH[0] = 0;
-      }
-    if( mDepthStencilCreated!=DONT_CREATE )
-      {
-      mDepthStencilCreated = NOT_CREATED_YET;
-      mDepthStencilH[0] = 0;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// create SYSTEM or TREE framebuffers (those are just like normal FBOs, just hold information
-// that they were autocreated only for the Library's internal purposes (SYSTEM) or for using
-// inside a Tree of DistortedNodes (TREE)
-// SYSTEM surfaces do not get removed in onDestroy().
-
-  DistortedFramebuffer(int numcolors, int depthStencil, int type, int width, int height)
-    {
-    super(width,height,NOT_CREATED_YET,numcolors,depthStencil,NOT_CREATED_YET, type);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create new offscreen Framebuffer with configurable number of COLOR, DEPTH and STENCIL attachments.
- *
- * @param width        Width of all the COLOR attachments.
- * @param height       Height of all the COLOR attachments.
- * @param numcolors    How many COLOR attachments to create?
- * @param depthStencil Add DEPTH or STENCIL attachment?
- *                     Valid values: NO_DEPTH_NO_STENCIL, DEPTH_NO_STENCIL, BOTH_DEPTH_STENCIL.
- */
-  @SuppressWarnings("unused")
-  public DistortedFramebuffer(int width, int height, int numcolors, int depthStencil)
-    {
-    super(width,height,NOT_CREATED_YET,numcolors,depthStencil,NOT_CREATED_YET,TYPE_USER);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-/**
- * Create new offscreen Framebuffer with COLOR0 attachment only.
- *
- * @param width Width of the COLOR0 attachment.
- * @param height Height of the COLOR0 attachment.
- */
-  @SuppressWarnings("unused")
-  public DistortedFramebuffer(int width, int height)
-    {
-    super(width,height,NOT_CREATED_YET, 1, NO_DEPTH_NO_STENCIL,NOT_CREATED_YET,TYPE_USER);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Bind the underlying rectangle of pixels as a OpenGL Texture.
- *
- * @return <code>true</code> if successful.
- */
-  public boolean setAsInput()
-    {
-    if( mColorH[0]>0 )
-      {
-      GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
-      return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Enable.disable DEPTH and STENCIL buffers.
- *
- * @param depthStencil Valid values: NO_DEPTH_NO_STENCIL, DEPTH_NO_STENCIL, BOTH_DEPTH_STENCIL.
- */
-  public void enableDepthStencil(int depthStencil)
-    {
-    if( depthStencil != mDepthStencil )
-      {
-      mDepthStencil = depthStencil;
-
-      if( depthStencil!= NO_DEPTH_NO_STENCIL && mDepthStencilCreated==DONT_CREATE )
-        {
-        mDepthStencilCreated = NOT_CREATED_YET;
-        markForCreation();
-        }
-      if( depthStencil== NO_DEPTH_NO_STENCIL && mDepthStencilCreated!=DONT_CREATE )
-        {
-        mDepthStencilCreated = DONT_CREATE;
-        markForCreation();
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Return the ID of the Texture (COLOR attachment 0) that's backing this FBO.
- * <p>
- * Catch: this will only work if the library has had time to actually create the texture. Remember
- * that the texture gets created only on first render, thus creating a Texture object and immediately
- * calling this method will return an invalid (negative) result.
- *
- * @return If there was not a single render between creation of the Object and calling this method on
- *         it, return a negative value. Otherwise, return ID of COLOR attachment 0.
- */
-  public int getTextureID()
-    {
-    return mColorH[0];
-    }
-  }
diff --git a/src/main/java/org/distorted/library/DistortedInputSurface.java b/src/main/java/org/distorted/library/DistortedInputSurface.java
deleted file mode 100644
index 5f5004f..0000000
--- a/src/main/java/org/distorted/library/DistortedInputSurface.java
+++ /dev/null
@@ -1,48 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * A Surface that we can set as an Input (take its rectangle of pixels and skin a Mesh with it).
- */
-
-public interface DistortedInputSurface
-{
-/**
- * Do not document this as public API
- * @y.exclude
- */
-  long getID();
-/**
- * Do not document this as public API
- * @y.exclude
- */
-  int getWidth();
-/**
- * Do not document this as public API
- * @y.exclude
- */
-  int getHeight();
-/**
- * Take the underlying rectangle of pixels and bind this texture to OpenGL.
- */
-  boolean setAsInput();
-}
diff --git a/src/main/java/org/distorted/library/DistortedMaster.java b/src/main/java/org/distorted/library/DistortedMaster.java
deleted file mode 100644
index 2b401cf..0000000
--- a/src/main/java/org/distorted/library/DistortedMaster.java
+++ /dev/null
@@ -1,111 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.util.ArrayList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * This static class handles assigning jobs to other classes. It does it once, at the beginning of
- * each frame.
- */
-class DistortedMaster
-  {
-  private static ArrayList<DistortedSlave> mSlaves = new ArrayList<>();
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private DistortedMaster()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static boolean toDo()
-    {
-    DistortedSlave slave;
-    int num = mSlaves.size();
-
-    for(int i=0; i<num; i++)
-      {
-      slave = mSlaves.remove(0);
-      slave.doWork();
-      }
-
-    return ( num>0 );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void newSlave(DistortedSlave s)
-    {
-    int num = mSlaves.size();
-    boolean found = false;
-    DistortedSlave tmp;
-
-    for(int i=0; i<num; i++)
-      {
-      tmp = mSlaves.get(i);
-
-      if( tmp==s )
-        {
-        found = true;
-        break;
-        }
-      }
-
-    if( !found ) mSlaves.add(s);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// can make this logarithmic but the typical number of children is very small anyway
-
-  static void addSorted(ArrayList<DistortedNode> mChildren, DistortedNode newChild)
-    {
-    DistortedNode child;
-    DistortedEffectsPostprocess dep;
-    int i,num = mChildren.size();
-    long bucket, newBucket;
-
-    dep = newChild.getEffectsPostprocess();
-    newBucket = dep==null ? 0 : dep.getBucket();
-
-    for(i=0; i<num; i++)
-      {
-      child = mChildren.get(i);
-      dep   = child.getEffectsPostprocess();
-      bucket= dep==null ? 0 : dep.getBucket();
-
-      if( bucket>newBucket ) break;
-      }
-
-    mChildren.add(i,newChild);
-
-    //android.util.Log.e("newChild", "newBucket="+newBucket+" new child at "+i+" total num ="+num);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void onDestroy()
-    {
-    mSlaves.clear();
-    }
-  }
diff --git a/src/main/java/org/distorted/library/DistortedNode.java b/src/main/java/org/distorted/library/DistortedNode.java
deleted file mode 100644
index fe141e4..0000000
--- a/src/main/java/org/distorted/library/DistortedNode.java
+++ /dev/null
@@ -1,886 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Class which represents a Node in a Tree of (InputSurface,Mesh,Effects) triplets.
- * <p>
- * Having organized such sets into a Tree, we can then render any Node to any OutputSurface.
- * That recursively renders the set held in the Node and all its children.
- * <p>
- * The class takes special care to only render identical sub-trees once. Each Node holds a reference
- * to sub-class 'NodeData'. Two identical sub-trees attached at different points of the main tree
- * will point to the same NodeData; only the first of this is rendered (mData.numRender!).
- */
-public class DistortedNode implements DistortedSlave
-  {
-  private static final int ATTACH = 0;
-  private static final int DETACH = 1;
-  private static final int DETALL = 2;
-  private static final int SORT   = 3;
-
-  private ArrayList<DistortedNode> mChildren;
-  private int[] mNumChildren;  // ==mChildren.length(), but we only create mChildren if the first one gets added
-
-  private class Job
-    {
-    int type;
-    DistortedNode node;
-    DistortedEffectsPostprocess dep;
-
-    Job(int t, DistortedNode n, DistortedEffectsPostprocess d)
-      {
-      type = t;
-      node = n;
-      dep  = d;
-      }
-    }
-
-  private ArrayList<Job> mJobs = new ArrayList<>();
-
-  private static HashMap<ArrayList<Long>,NodeData> mMapNodeID = new HashMap<>();
-  private static long mNextNodeID =0;
-
-  private DistortedNode mParent;
-  private DistortedOutputSurface mSurfaceParent;
-  private MeshObject mMesh;
-  private DistortedEffects mEffects;
-  private DistortedEffectsPostprocess mPostprocess;
-  private DistortedInputSurface mSurface;
-  private DistortedRenderState mState;
-  private NodeData mData;
-  private int mFboW, mFboH, mFboDepthStencil;
-
-  private class NodeData
-    {
-    long ID;
-    int numPointingNodes;
-    long currTime;
-    ArrayList<Long> key;
-    DistortedFramebuffer mFBO;
-
-    NodeData(long id, ArrayList<Long> k)
-      {
-      ID              = id;
-      key             = k;
-      numPointingNodes= 1;
-      currTime        =-1;
-      mFBO            = null;
-      }
-    }
- 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static synchronized void onPause()
-    {
-    NodeData data;
-
-    for (HashMap.Entry<ArrayList<Long>,NodeData> entry : mMapNodeID.entrySet())
-      {
-      data = entry.getValue();
-
-      if( data.mFBO != null )
-        {
-        data.mFBO.markForDeletion();
-        data.mFBO = null;
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static synchronized void onDestroy()
-    {
-    mNextNodeID = 0;
-    mMapNodeID.clear();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private ArrayList<Long> generateIDList()
-    {
-    ArrayList<Long> ret = new ArrayList<>();
-
-    if( mNumChildren[0]==0 )
-      {
-      // add a negative number so this leaf never gets confused with a internal node
-      // with a single child that happens to have ID identical to some leaf's Effects ID.
-      ret.add(-mEffects.getID());
-      }
-    else
-      {
-      DistortedNode node;
-   
-      for(int i=0; i<mNumChildren[0]; i++)
-        {
-        node = mChildren.get(i);
-        ret.add(node.mData.ID);
-        }
-
-      // A bit questionable decision here - we are sorting the children IDs, which means
-      // that order in which we draw the children is going to be undefined (well, this is not
-      // strictly speaking true - when rendering, if no postprocessing and isomorphism are
-      // involved, we *DO* render the children in order they were added; if however there
-      // are two internal nodes with the same list of identical children, just added in a
-      // different order each time, then we consider them isomorphic, i.e. identical and only
-      // render the first one. If then two children of such 'pseudo-isomorphic' nodes are at
-      // exactly the same Z-height this might result in some unexpected sights).
-      //
-      // Reason: with the children being sorted by postprocessing buckets, the order is
-      // undefined anyway (although only when postprocessing is applied).
-      //
-      // See the consequences in the 'Olympic' app - remove a few leaves and add them back in
-      // different order. You will see the number of renders go back to the original 14.
-      Collections.sort(ret);
-      }
-
-    ret.add( 0, mSurface.getID() );
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Debug - print all the Node IDs
-
-  @SuppressWarnings("unused")
-  void debug(int depth)
-    {
-    String tmp="";
-    int i;
-
-    for(i=0; i<depth; i++) tmp +="   ";
-    tmp += ("NodeID="+mData.ID+" nodes pointing: "+mData.numPointingNodes+" surfaceID="+
-            mSurface.getID()+" FBO="+(mData.mFBO==null ? "null":mData.mFBO.getID()))+
-            " parent sID="+(mParent==null ? "null": (mParent.mSurface.getID()));
-
-    android.util.Log.e("NODE", tmp);
-
-    for(i=0; i<mNumChildren[0]; i++)
-      mChildren.get(i).debug(depth+1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Debug - print contents of the HashMap
-
-  @SuppressWarnings("unused")
-  static void debugMap()
-    {
-    NodeData tmp;
-
-    for(ArrayList<Long> key: mMapNodeID.keySet())
-      {
-      tmp = mMapNodeID.get(key);
-      android.util.Log.e("NODE", "NodeID: "+tmp.ID+" <-- "+key);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// tree isomorphism algorithm
-
-  private void adjustIsomorphism()
-    {
-    ArrayList<Long> newList = generateIDList();
-    NodeData newData = mMapNodeID.get(newList);
-
-    if( newData!=null )
-      {
-      newData.numPointingNodes++;
-      }
-    else
-      {
-      newData = new NodeData(++mNextNodeID,newList);
-      mMapNodeID.put(newList,newData);
-      }
-
-    boolean deleteOldFBO = false;
-    boolean createNewFBO = false;
-
-    if( --mData.numPointingNodes==0 )
-      {
-      mMapNodeID.remove(mData.key);
-      if( mData.mFBO!=null ) deleteOldFBO=true;
-      }
-    if( mNumChildren[0]>0 && newData.mFBO==null )
-      {
-      createNewFBO = true;
-      }
-    if( mNumChildren[0]==0 && newData.mFBO!=null )
-      {
-      newData.mFBO.markForDeletion();
-      android.util.Log.d("NODE", "ERROR!! this NodeData cannot possibly contain a non-null FBO!! "+newData.mFBO.getID() );
-      newData.mFBO = null;
-      }
-
-    if( deleteOldFBO && createNewFBO )
-      {
-      newData.mFBO = mData.mFBO;  // just copy over
-      //android.util.Log.d("NODE", "copying over FBOs "+mData.mFBO.getID() );
-      }
-    else if( deleteOldFBO )
-      {
-      mData.mFBO.markForDeletion();
-      //android.util.Log.d("NODE", "deleting old FBO "+mData.mFBO.getID() );
-      mData.mFBO = null;
-      }
-    else if( createNewFBO )
-      {
-      int width  = mFboW <= 0 ? mSurface.getWidth()  : mFboW;
-      int height = mFboH <= 0 ? mSurface.getHeight() : mFboH;
-      newData.mFBO = new DistortedFramebuffer(1,mFboDepthStencil, DistortedSurface.TYPE_TREE, width, height);
-      //android.util.Log.d("NODE", "creating new FBO "+newData.mFBO.getID() );
-      }
-
-    mData = newData;
-
-    if( mParent!=null ) mParent.adjustIsomorphism();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int markStencilAndDepth(long currTime, DistortedOutputSurface surface, DistortedEffectsPostprocess effects)
-    {
-    DistortedInputSurface input = mNumChildren[0]==0 ? mSurface : mData.mFBO;
-
-    if( input.setAsInput() )
-      {
-      surface.setAsOutput();
-      DistortedRenderState.setUpStencilMark();
-      mEffects.drawPriv(mSurface.getWidth() /2.0f, mSurface.getHeight()/2.0f, mMesh, surface, currTime, effects.getHalo()*surface.mMipmap);
-      DistortedRenderState.unsetUpStencilMark();
-
-      return 1;
-      }
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return the total number of render calls issued
-
-  int draw(long currTime, DistortedOutputSurface surface)
-    {
-    DistortedInputSurface input = mNumChildren[0]==0 ? mSurface : mData.mFBO;
-
-    if( input.setAsInput() )
-      {
-      surface.setAsOutput(currTime);
-      mState.apply();
-      mEffects.drawPriv(mSurface.getWidth()/2.0f, mSurface.getHeight()/2.0f, mMesh, surface, currTime, 0);
-      return 1;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return the total number of render calls issued
-
-  int renderRecursive(long currTime)
-    {
-    int numRenders = 0;
-
-    if( mNumChildren[0]>0 && mData.currTime!=currTime )
-      {
-      mData.currTime = currTime;
-
-      for (int i=0; i<mNumChildren[0]; i++)
-        {
-        numRenders += mChildren.get(i).renderRecursive(currTime);
-        }
-
-      if( mData.mFBO==null )
-        {
-        int width  = mFboW <= 0 ? mSurface.getWidth()  : mFboW;
-        int height = mFboH <= 0 ? mSurface.getHeight() : mFboH;
-        mData.mFBO = new DistortedFramebuffer(1,mFboDepthStencil, DistortedSurface.TYPE_TREE, width, height);
-        }
-
-      mData.mFBO.setAsOutput(currTime);
-
-      if( mSurface.setAsInput() )
-        {
-        numRenders++;
-        DistortedEffects.blitPriv(mData.mFBO);
-        }
-
-      numRenders += mData.mFBO.renderChildren(currTime,mNumChildren[0],mChildren);
-      }
-
-    return numRenders;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void newJob(int t, DistortedNode n, DistortedEffectsPostprocess d)
-    {
-    mJobs.add(new Job(t,n,d));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setPost(DistortedEffectsPostprocess dep)
-    {
-    mPostprocess = dep;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setSurfaceParent(DistortedOutputSurface dep)
-    {
-    mSurfaceParent = dep;
-    mParent = null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Constructs new Node.
- *     
- * @param surface InputSurface to put into the new Node.
- * @param effects DistortedEffects to put into the new Node.
- * @param mesh MeshObject to put into the new Node.
- */
-  public DistortedNode(DistortedInputSurface surface, DistortedEffects effects, MeshObject mesh)
-    {
-    mSurface       = surface;
-    mEffects       = effects;
-    mPostprocess   = null;
-    mMesh          = mesh;
-    mState         = new DistortedRenderState();
-    mChildren      = null;
-    mNumChildren   = new int[1];
-    mNumChildren[0]= 0;
-    mParent        = null;
-    mSurfaceParent = null;
-
-    mFboW            = 0;  // i.e. take this from
-    mFboH            = 0;  // mSurface's dimensions
-    mFboDepthStencil = DistortedFramebuffer.DEPTH_NO_STENCIL;
-
-    ArrayList<Long> list = new ArrayList<>();
-    list.add(mSurface.getID());
-    list.add(-mEffects.getID());
-
-    mData = mMapNodeID.get(list);
-   
-    if( mData!=null )
-      {
-      mData.numPointingNodes++;
-      }
-    else
-      {
-      mData = new NodeData(++mNextNodeID,list);
-      mMapNodeID.put(list, mData);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////  
-/**
- * Copy-constructs new Node from another Node.
- *     
- * @param node The DistortedNode to copy data from.
- * @param flags bit field composed of a subset of the following:
- *        {@link Distorted#CLONE_SURFACE},  {@link Distorted#CLONE_MATRIX}, {@link Distorted#CLONE_VERTEX},
- *        {@link Distorted#CLONE_FRAGMENT} and {@link Distorted#CLONE_CHILDREN}.
- *        For example flags = CLONE_SURFACE | CLONE_CHILDREN.
- */
-  public DistortedNode(DistortedNode node, int flags)
-    {
-    mEffects      = new DistortedEffects(node.mEffects,flags);
-    mPostprocess  = null;
-    mMesh         = node.mMesh;
-    mState        = new DistortedRenderState();
-    mParent       = null;
-    mSurfaceParent= null;
-
-    mFboW            = node.mFboW;
-    mFboH            = node.mFboH;
-    mFboDepthStencil = node.mFboDepthStencil;
-
-    if( (flags & Distorted.CLONE_SURFACE) != 0 )
-      {
-      mSurface = node.mSurface;
-      }
-    else
-      {
-      int w = node.mSurface.getWidth();
-      int h = node.mSurface.getHeight();
-
-      if( node.mSurface instanceof DistortedTexture )
-        {
-        mSurface = new DistortedTexture(w,h, DistortedSurface.TYPE_TREE);
-        }
-      else if( node.mSurface instanceof DistortedFramebuffer )
-        {
-        int depthStencil = DistortedFramebuffer.NO_DEPTH_NO_STENCIL;
-
-        if( ((DistortedFramebuffer) node.mSurface).hasDepth() )
-          {
-          boolean hasStencil = ((DistortedFramebuffer) node.mSurface).hasStencil();
-          depthStencil = (hasStencil ? DistortedFramebuffer.BOTH_DEPTH_STENCIL:DistortedFramebuffer.DEPTH_NO_STENCIL);
-          }
-
-        mSurface = new DistortedFramebuffer(1,depthStencil,DistortedSurface.TYPE_TREE,w,h);
-        }
-      }
-    if( (flags & Distorted.CLONE_CHILDREN) != 0 )
-      {
-      if( node.mChildren==null )     // do NOT copy over the NULL!
-        {
-        node.mChildren = new ArrayList<>(2);
-        }
-
-      mChildren = node.mChildren;
-      mNumChildren = node.mNumChildren;
-      }
-    else
-      {
-      mChildren = null;
-      mNumChildren = new int[1];
-      mNumChildren[0] = 0;
-      }
-   
-    ArrayList<Long> list = generateIDList();
-   
-    mData = mMapNodeID.get(list);
-   
-    if( mData!=null )
-      {
-      mData.numPointingNodes++;
-      }
-    else
-      {
-      mData = new NodeData(++mNextNodeID,list);
-      mMapNodeID.put(list, mData);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Adds a new child to the last position in the list of our Node's children.
- * <p>
- * We cannot do this mid-render - actual attachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param node The new Node to add.
- */
-  public void attach(DistortedNode node)
-    {
-    mJobs.add(new Job(ATTACH,node,null));
-    DistortedMaster.newSlave(this);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Adds a new child to the last position in the list of our Node's children.
- * <p>
- * We cannot do this mid-render - actual attachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param surface InputSurface to initialize our child Node with.
- * @param effects DistortedEffects to initialize our child Node with.
- * @param mesh MeshObject to initialize our child Node with.
- * @return the newly constructed child Node, or null if we couldn't allocate resources.
- */
-  public DistortedNode attach(DistortedInputSurface surface, DistortedEffects effects, MeshObject mesh)
-    {
-    DistortedNode node = new DistortedNode(surface,effects,mesh);
-    mJobs.add(new Job(ATTACH,node,null));
-    DistortedMaster.newSlave(this);
-    return node;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes the first occurrence of a specified child from the list of children of our Node.
- * <p>
- * We cannot do this mid-render - actual detachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param node The Node to remove.
- */
-  public void detach(DistortedNode node)
-    {
-    mJobs.add(new Job(DETACH,node,null));
-    DistortedMaster.newSlave(this);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes the first occurrence of a specified child from the list of children of our Node.
- * <p>
- * A bit questionable method as there can be many different Nodes attached as children, some
- * of them having the same Effects but - for instance - different Mesh. Use with care.
- * <p>
- * We cannot do this mid-render - actual detachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param effects DistortedEffects to remove.
- */
-  public void detach(DistortedEffects effects)
-    {
-    long id = effects.getID();
-    DistortedNode node;
-    boolean detached = false;
-
-    for(int i=0; i<mNumChildren[0]; i++)
-      {
-      node = mChildren.get(i);
-
-      if( node.getEffects().getID()==id )
-        {
-        detached = true;
-        mJobs.add(new Job(DETACH,node,null));
-        DistortedMaster.newSlave(this);
-        break;
-        }
-      }
-
-    if( !detached )
-      {
-      // if we failed to detach any, it still might be the case that
-      // there's an ATTACH job that we need to cancel.
-      int num = mJobs.size();
-      Job job;
-
-      for(int i=0; i<num; i++)
-        {
-        job = mJobs.get(i);
-
-        if( job.type==ATTACH && job.node.getEffects()==effects )
-          {
-          mJobs.remove(i);
-          break;
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes all children Nodes.
- * <p>
- * We cannot do this mid-render - actual detachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- */
-  public void detachAll()
-    {
-    mJobs.add(new Job(DETALL,null,null));
-    DistortedMaster.newSlave(this);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * This is not really part of the public API. Has to be public only because it is a part of the
- * DistortedSlave interface, which should really be a class that we extend here instead but
- * Java has no multiple inheritance.
- *
- * @y.exclude
- */
-  public void doWork()
-    {
-    int num = mJobs.size();
-    Job job;
-
-    int numChanges=0;
-
-    for(int i=0; i<num; i++)
-      {
-      job = mJobs.remove(0);
-
-      switch(job.type)
-        {
-        case ATTACH: numChanges++;
-                     if( mChildren==null ) mChildren = new ArrayList<>(2);
-                     job.node.mParent = this;
-                     job.node.mSurfaceParent = null;
-                     DistortedMaster.addSorted(mChildren,job.node);
-                     mNumChildren[0]++;
-                     break;
-        case DETACH: numChanges++;
-                     if( mNumChildren[0]>0 && mChildren.remove(job.node) )
-                       {
-                       job.node.mParent = null;
-                       job.node.mSurfaceParent = null;
-                       mNumChildren[0]--;
-                       }
-                     break;
-        case DETALL: numChanges++;
-                     if( mNumChildren[0]>0 )
-                       {
-                       DistortedNode tmp;
-
-                       for(int j=mNumChildren[0]-1; j>=0; j--)
-                         {
-                         tmp = mChildren.remove(j);
-                         tmp.mParent = null;
-                         tmp.mSurfaceParent = null;
-                         }
-
-                       mNumChildren[0] = 0;
-                       }
-                     break;
-        case SORT  : job.node.mPostprocess = job.dep;
-                     mChildren.remove(job.node);
-                     DistortedMaster.addSorted(mChildren,job.node);
-                     break;
-        }
-      }
-
-    if( numChanges>0 ) adjustIsomorphism();
-    }
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Sets the Postprocessing Effects we will apply to the temporary buffer this Node - and fellow siblings
- * with the same Effects - will get rendered to.
- * <p>
- * For efficiency reasons, it is very important to assign the very same DistortedEffectsPostprocess
- * object to all the DistortedNode siblings that are supposed to be postprocessed in the same way,
- * because only then will the library assign all such siblings to the same 'Bucket' which gets rendered
- * to the same offscreen buffer which then gets postprocessed in one go and subsequently merged to the
- * target Surface.
- */
-  public void setPostprocessEffects(DistortedEffectsPostprocess dep)
-    {
-    if( mParent!=null )
-      {
-      mParent.newJob(SORT, this, dep);
-      DistortedMaster.newSlave(mParent);
-      }
-    else if( mSurfaceParent!=null )
-      {
-      mSurfaceParent.newJob(SORT, this, dep);
-      DistortedMaster.newSlave(mSurfaceParent);
-      }
-    else
-      {
-      mPostprocess = dep;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns the DistortedEffectsPostprocess object that's in the Node.
- *
- * @return The DistortedEffectsPostprocess contained in the Node.
- */
-  public DistortedEffectsPostprocess getEffectsPostprocess()
-    {
-    return mPostprocess;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns the DistortedEffects object that's in the Node.
- * 
- * @return The DistortedEffects contained in the Node.
- */
-  public DistortedEffects getEffects()
-    {
-    return mEffects;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Returns the DistortedInputSurface object that's in the Node.
- *
- * @return The DistortedInputSurface contained in the Node.
- */
-  public DistortedInputSurface getSurface()
-    {
-    return mSurface;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Resizes the DistortedFramebuffer object that we render this Node to.
- */
-  public void resize(int width, int height)
-    {
-    mFboW = width;
-    mFboH = height;
-
-    if ( mData.mFBO !=null )
-      {
-      // TODO: potentially allocate a new NodeData if we have to
-      mData.mFBO.resize(width,height);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Enables/disables DEPTH and STENCIL buffers in the Framebuffer object that we render this Node to.
- */
-  public void enableDepthStencil(int depthStencil)
-    {
-    mFboDepthStencil = depthStencil;
-
-    if ( mData.mFBO !=null )
-      {
-      // TODO: potentially allocate a new NodeData if we have to
-      mData.mFBO.enableDepthStencil(depthStencil);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// APIs that control how to set the OpenGL state just before rendering this Node.
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, use ColorMask (r,g,b,a).
- *
- * @param r Write to the RED color channel when rendering this Node?
- * @param g Write to the GREEN color channel when rendering this Node?
- * @param b Write to the BLUE color channel when rendering this Node?
- * @param a Write to the ALPHA channel when rendering this Node?
- */
-  @SuppressWarnings("unused")
-  public void glColorMask(boolean r, boolean g, boolean b, boolean a)
-    {
-    mState.glColorMask(r,g,b,a);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, switch on writing to Depth buffer?
- *
- * @param mask Write to the Depth buffer when rendering this Node?
- */
-  @SuppressWarnings("unused")
-  public void glDepthMask(boolean mask)
-    {
-    mState.glDepthMask(mask);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, which bits of the Stencil buffer to write to?
- *
- * @param mask Marks the bits of the Stencil buffer we will write to when rendering this Node.
- */
-  @SuppressWarnings("unused")
-  public void glStencilMask(int mask)
-    {
-    mState.glStencilMask(mask);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, which Tests to enable?
- *
- * @param test Valid values: GL_DEPTH_TEST, GL_STENCIL_TEST, GL_BLEND
- */
-  @SuppressWarnings("unused")
-  public void glEnable(int test)
-    {
-    mState.glEnable(test);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, which Tests to enable?
- *
- * @param test Valid values: GL_DEPTH_TEST, GL_STENCIL_TEST, GL_BLEND
- */
-  @SuppressWarnings("unused")
-  public void glDisable(int test)
-    {
-    mState.glDisable(test);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, use the following StencilFunc.
- *
- * @param func Valid values: GL_NEVER, GL_ALWAYS, GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATER, GL_NOTEQUAL
- * @param ref  Reference valut to compare our stencil with.
- * @param mask Mask used when comparing.
- */
-  @SuppressWarnings("unused")
-  public void glStencilFunc(int func, int ref, int mask)
-    {
-    mState.glStencilFunc(func,ref,mask);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, use the following StencilOp.
- * <p>
- * Valid values of all 3 parameters: GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT, GL_INCR_WRAP, GL_DECR_WRAP
- *
- * @param sfail  What to do when Stencil Test fails.
- * @param dpfail What to do when Depth Test fails.
- * @param dppass What to do when Depth Test passes.
- */
-  @SuppressWarnings("unused")
-  public void glStencilOp(int sfail, int dpfail, int dppass)
-    {
-    mState.glStencilOp(sfail,dpfail,dppass);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, use the following DepthFunc.
- *
- * @param func Valid values: GL_NEVER, GL_ALWAYS, GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATER, GL_NOTEQUAL
- */
-  @SuppressWarnings("unused")
-  public void glDepthFunc(int func)
-    {
-    mState.glDepthFunc(func);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Node, use the following Blending mode.
- * <p>
- * Valid values: GL_ZERO, GL_ONE, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
- *               GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_CONSTANT_COLOR, GL_ONE_MINUS_CONSTANT_COLOR,
- *               GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA, GL_SRC_ALPHA_SATURATE
- *
- * @param src Source Blend function
- * @param dst Destination Blend function
- */
-  @SuppressWarnings("unused")
-  public void glBlendFunc(int src, int dst)
-    {
-    mState.glBlendFunc(src,dst);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Before rendering this Node, clear the following buffers.
- * <p>
- * Valid values: 0, or bitwise OR of one or more values from the set GL_COLOR_BUFFER_BIT,
- *               GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT.
- * Default: 0
- *
- * @param mask bitwise OR of BUFFER_BITs to clear.
- */
-  @SuppressWarnings("unused")
-  public void glClear(int mask)
-    {
-    mState.glClear(mask);
-    }
-  }
diff --git a/src/main/java/org/distorted/library/DistortedObject.java b/src/main/java/org/distorted/library/DistortedObject.java
deleted file mode 100644
index 5a29373..0000000
--- a/src/main/java/org/distorted/library/DistortedObject.java
+++ /dev/null
@@ -1,233 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.util.HashMap;
-import java.util.LinkedList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Any Object which gets uploaded to GPU memory and thus needs to be re-created (transparently to
- * applications!) whenever we lose OpenGL context.
- *
- * Keep all objects created in a static LinkedList. The point: we need to be able to mark
- * Objects for deletion, and delete all marked Objects later at a convenient time (that's
- * because we can only delete from a thread that holds the OpenGL context so here we provide a
- * framework where one is able to mark for deletion at any time and actual deletion takes place
- * on the next render).
-*/
-abstract class DistortedObject
-{
-  static final int FAILED_TO_CREATE = 1;
-  static final int NOT_CREATED_YET  = 2;
-  static final int DONT_CREATE      = 3;
-  static final int CREATED          = 4;
-
-  static final int TYPE_USER = 0;
-  static final int TYPE_TREE = 1;
-  static final int TYPE_SYST = 2;
-
-  private static final int JOB_CREATE = 0;
-  private static final int JOB_DELETE = 1;
-
-  private class Job
-    {
-    DistortedObject object;
-    int action;
-
-    Job(DistortedObject o, int a)
-      {
-      object = o;
-      action = a;
-      }
-    }
-
-  private static boolean mToDo = false;
-  private static LinkedList<DistortedObject> mDoneList = new LinkedList<>();
-  private static HashMap<Long,Job> mToDoMap = new HashMap<>();
-  private static long mNextClientID = 0;
-  private static long mNextSystemID = 0;
-
-  private long mID;
-  private int mType;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract void create();
-  abstract void delete();
-  abstract void recreate();
-  abstract String printDetails();
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// must be called from a thread holding OpenGL Context
-
-  static synchronized boolean toDo()
-    {
-    if( mToDo )
-      {
-      Job job;
-      DistortedObject object;
-
-      for(Long key: mToDoMap.keySet())
-        {
-        job = mToDoMap.get(key);
-        object = job.object;
-
-        //android.util.Log.d("Object", object.getClass().getSimpleName()+"  ---> need to "
-        //                  +(job.action==JOB_CREATE ? "create":"delete")+" objectID="+object.mID );
-
-        if( job.action==JOB_CREATE )
-          {
-          object.create();
-          mDoneList.add(object);
-          }
-        else if( job.action==JOB_DELETE )
-          {
-          object.delete();
-          }
-        }
-
-      mToDoMap.clear();
-      mToDo = false;
-      return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static synchronized void onPause()
-    {
-    DistortedObject object;
-    int num = mDoneList.size();
-
-    for(int i=0; i<num; i++)
-      {
-      object = mDoneList.removeFirst();
-      mToDoMap.put(object.mID, object.new Job(object,JOB_CREATE) );
-      object.recreate();
-      }
-
-    mToDo = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static synchronized void onDestroy()
-    {
-    mToDoMap.clear();
-    mDoneList.clear();
-
-    mToDo = true;
-    mNextClientID = 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  @SuppressWarnings("unused")
-  static void debugLists()
-    {
-    android.util.Log.e("Object", "Done list:");
-
-    DistortedObject object;
-    int num = mDoneList.size();
-
-    for(int i=0; i<num; i++)
-      {
-      object = mDoneList.get(i);
-      object.print("");
-      }
-
-    android.util.Log.e("Object", "ToDo list:");
-
-    Job job;
-
-    for(Long key: mToDoMap.keySet())
-      {
-      job = mToDoMap.get(key);
-      job.object.print(job.action==JOB_CREATE ? " create":" delete");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void print(String msg)
-    {
-    String str = "ID:"+mID;
-
-    switch(mType)
-      {
-      case TYPE_SYST: str+=" SYSTEM "; break;
-      case TYPE_USER: str+=" USER   "; break;
-      case TYPE_TREE: str+=" TREE   "; break;
-      default       : str+=" ERROR? ";
-      }
-
-    android.util.Log.e("Object", str+printDetails()+msg);
-    }
-
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  DistortedObject(int create, int type)
-    {
-    mID  = type==TYPE_SYST ? --mNextSystemID : ++mNextClientID;
-    mType= type;
-
-    if( create!=DONT_CREATE )
-      {
-      mToDoMap.put(mID, new Job(this,JOB_CREATE) );
-      mToDo = true;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized void markForCreation()
-    {
-    mDoneList.remove(this);
-    mToDoMap.put(mID, new Job(this,JOB_CREATE) );
-    mToDo = true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Mark the underlying OpenGL object for deletion. Actual deletion will take place on the next render.
- */
-  synchronized public void markForDeletion()
-    {
-    mDoneList.remove(this);
-    mToDoMap.put(mID, new Job(this,JOB_DELETE) );
-    mToDo = true;
-    }
-
-////////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Return unique ID of this Object.
- */
-  public long getID()
-    {
-    return mID;
-    }
-
-}
diff --git a/src/main/java/org/distorted/library/DistortedOutputSurface.java b/src/main/java/org/distorted/library/DistortedOutputSurface.java
deleted file mode 100644
index ab1f611..0000000
--- a/src/main/java/org/distorted/library/DistortedOutputSurface.java
+++ /dev/null
@@ -1,764 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import android.opengl.GLES30;
-import android.opengl.Matrix;
-
-import java.util.ArrayList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class DistortedOutputSurface extends DistortedSurface implements DistortedSlave
-{
-//////////// DEBUG FLAGS /////////////////////////////////////////////
-/**
- * When rendering a Screen, show FPS in the upper-left corner?
- */
-public static final int DEBUG_FPS = 1;
-//////////// END DEBUG FLAGS /////////////////////////////////////////
-
-/**
- * Do not create DEPTH or STENCIL attachment
- */
-  public static final int NO_DEPTH_NO_STENCIL = 0;
-/**
- * Create DEPTH, but not STENCIL
- */
-  public static final int DEPTH_NO_STENCIL    = 1;
-/**
- * Create both DEPTH and STENCIL
- */
-  public static final int BOTH_DEPTH_STENCIL  = 2;
-
-  private static final int ATTACH = 0;
-  private static final int DETACH = 1;
-  private static final int DETALL = 2;
-  private static final int SORT   = 3;
-
-  private ArrayList<DistortedNode> mChildren;
-  private int mNumChildren;   // ==mChildren.length(), but we only create mChildren if the first one gets added
-
-  private class Job
-    {
-    int type;
-    DistortedNode node;
-    DistortedEffectsPostprocess dep;
-
-    Job(int t, DistortedNode n, DistortedEffectsPostprocess d)
-      {
-      type = t;
-      node = n;
-      dep  = d;
-      }
-    }
-
-  private ArrayList<Job> mJobs = new ArrayList<>();
-
-  DistortedFramebuffer[] mBuffer;
-
-  private long mTime;
-  private float mFOV;
-  float mDistance, mNear;
-  float[] mProjectionMatrix;
-
-  int mDepthStencilCreated;
-  int mDepthStencil;
-  int[] mDepthStencilH = new int[1];
-  int[] mFBOH          = new int[1];
-
-  private float mClearR, mClearG, mClearB, mClearA;
-  private float mClearDepth;
-  private int mClearStencil;
-  private int mClear;
-  float mMipmap;
-
-  private int mDebugLevel;
-
-private String sLast="", sCurr="";
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract void prepareDebug(long time);
-  abstract void renderDebug(long time);
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  DistortedOutputSurface(int width, int height, int createColor, int numcolors, int depthStencil, int fbo, int type)
-    {
-    super(width,height,createColor,numcolors,type);
-
-    mProjectionMatrix = new float[16];
-
-    mFOV = 60.0f;
-    mNear=  0.5f;
-
-    mDepthStencilCreated= (depthStencil== NO_DEPTH_NO_STENCIL ? DONT_CREATE:NOT_CREATED_YET);
-    mDepthStencil = depthStencil;
-
-    mFBOH[0]         = fbo;
-    mDepthStencilH[0]= 0;
-
-    mTime = 0;
-    mDebugLevel = 0;
-
-    mClearR = 0.0f;
-    mClearG = 0.0f;
-    mClearB = 0.0f;
-    mClearA = 0.0f;
-
-    mClearDepth = 1.0f;
-    mClearStencil = 0;
-    mClear = GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT;
-
-    mBuffer = new DistortedFramebuffer[EffectQuality.LENGTH];
-
-    mMipmap = 1.0f;
-
-    createProjection();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createProjection()
-    {
-    if( mWidth>0 && mHeight>1 )
-      {
-      if( mFOV>0.0f )  // perspective projection
-        {
-        float a = 2.0f*(float)Math.tan(mFOV*Math.PI/360);
-        float q = mWidth*mNear;
-        float c = mHeight*mNear;
-
-        float left   = -q/2;
-        float right  = +q/2;
-        float bottom = -c/2;
-        float top    = +c/2;
-        float near   =  c/a;
-
-        mDistance    = mHeight/a;
-        float far    = 2*mDistance-near;
-
-        Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
-        }
-      else             // parallel projection
-        {
-        float left   = -mWidth/2.0f;
-        float right  = +mWidth/2.0f;
-        float bottom = -mHeight/2.0f;
-        float top    = +mHeight/2.0f;
-        float near   = mWidth+mHeight-mHeight*(1.0f-mNear);
-        mDistance    = mWidth+mHeight;
-        float far    = mWidth+mHeight+mHeight*(1.0f-mNear);
-
-        Matrix.orthoM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void createBuffers()
-    {
-    float mipmap=1.0f;
-
-    for(int j=0; j<EffectQuality.LENGTH; j++)
-      {
-      mBuffer[j] = new DistortedFramebuffer(2,BOTH_DEPTH_STENCIL,TYPE_SYST, (int)(mWidth*mipmap), (int)(mHeight*mipmap) );
-      mBuffer[j].mMipmap = mipmap;
-      mipmap *= EffectQuality.MULTIPLIER;
-      }
-
-    DistortedObject.toDo(); // create the FBOs immediately. This is safe as we must be holding the OpenGL context now.
-
-    GLES30.glStencilMask(0xff);
-    GLES30.glDepthMask(true);
-    GLES30.glColorMask(true,true,true,true);
-    GLES30.glClearColor(0.0f,0.0f,0.0f,0.0f);
-    GLES30.glClearDepthf(1.0f);
-    GLES30.glClearStencil(0);
-
-    for(int j=0; j<EffectQuality.LENGTH; j++)
-      {
-      mBuffer[j].setAsOutput();
-      GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mBuffer[j].mColorH[1], 0);
-      GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT|GLES30.GL_DEPTH_BUFFER_BIT|GLES30.GL_STENCIL_BUFFER_BIT);
-      GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mBuffer[j].mColorH[0], 0);
-      GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int blitWithDepth(int quality, long currTime)
-    {
-    DistortedFramebuffer buffer = mBuffer[quality];
-
-    GLES30.glViewport(0, 0, mWidth, mHeight);
-    setAsOutput(currTime);
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mColorH[0]);
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE1);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mDepthStencilH[0]);
-
-    GLES30.glDisable(GLES30.GL_STENCIL_TEST);
-    GLES30.glStencilMask(0x00);
-
-    DistortedEffects.blitDepthPriv(this);
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE1);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
-
-    // clear buffers
-    GLES30.glStencilMask(0xff);
-    GLES30.glDepthMask(true);
-    GLES30.glColorMask(true,true,true,true);
-    GLES30.glClearColor(0.0f,0.0f,0.0f,0.0f);
-    GLES30.glClearDepthf(1.0f);
-    GLES30.glClearStencil(0);
-
-    buffer.setAsOutput();
-    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[1], 0);
-    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT|GLES30.GL_DEPTH_BUFFER_BIT|GLES30.GL_STENCIL_BUFFER_BIT);
-    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[0], 0);
-    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
-
-    return 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Render all children, one by one. If there are no postprocessing effects, just render to THIS.
-// Otherwise, render to a buffer and on each change of Postprocessing Bucket, apply the postprocessing
-// to a whole buffer and merge it.
-
-  int renderChildren(long time, int num, ArrayList<DistortedNode> children)
-    {
-    int numRenders = 0;
-    DistortedNode child1, child2;
-    DistortedEffectsPostprocess lastP=null, currP;
-    long lastB=0, currB;
-    int bucketChange=0;
-    int lastQ=0, currQ;
-
-sCurr = "";
-
-    for(int i=0; i<num; i++)
-      {
-      child1 = children.get(i);
-      currP = child1.getEffectsPostprocess();
-      currB = currP==null ? 0 : currP.getBucket();
-      currQ = currP==null ? 0 : currP.getQuality();
-
-sCurr += (" "+currB);
-
-      if( currB==0 ) numRenders += child1.draw(time,this);
-      else
-        {
-        if( mBuffer[0]==null ) createBuffers();
-
-        if( lastB!=currB )
-          {
-          if( lastB!=0 )
-            {
-            for(int j=bucketChange; j<i; j++)
-              {
-              child2 = children.get(j);
-              numRenders += child2.markStencilAndDepth(time,mBuffer[lastQ],lastP);
-              }
-
-            numRenders += lastP.postprocess(time, this);
-            numRenders += blitWithDepth(lastQ,time);
-            }
-
-          bucketChange = i;
-          }
-
-        numRenders += child1.draw(time,mBuffer[currQ]);
-
-        if( i==num-1 )
-          {
-          for(int j=bucketChange; j<num; j++)
-            {
-            child2 = children.get(j);
-            numRenders += child2.markStencilAndDepth(time,mBuffer[currQ],currP);
-            }
-
-          numRenders += currP.postprocess(time,this);
-          numRenders += blitWithDepth(currQ,time);
-          }
-        }
-
-      lastQ = currQ;
-      lastP = currP;
-      lastB = currB;
-      }
-
-if( !sLast.equals(sCurr) ) android.util.Log.e("surface", "rendering: "+sCurr);
-sLast = sCurr;
-
-    return numRenders;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void newJob(int t, DistortedNode n, DistortedEffectsPostprocess d)
-    {
-    mJobs.add(new Job(t,n,d));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setAsOutput()
-    {
-    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Make the library show various debugging information.
- * <p>
- * Currently only DEBUG_FPS - show FPS in the upper-left corner of every Screen - is defined.
- *
- * @param bitmask 0, or a bitmask of DEBUG_** flags to enable (currently only DEBUG_FPS defined)
- */
-  public void setDebug(int bitmask)
-    {
-    mDebugLevel = bitmask;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-/**
- * Draws all the attached children to this OutputSurface.
- * <p>
- * Must be called from a thread holding OpenGL Context.
- *
- * @param time Current time, in milliseconds. This will be passed to all the Effects stored in the children Nodes.
- * @return Number of objects rendered.
- */
-  public int render(long time)
-    {
-    boolean showDebug = ( mDebugLevel!=0 && this instanceof DistortedScreen );
-
-    if( showDebug ) prepareDebug(time);
-
-    // change tree topology (attach and detach children)
-/*
-    boolean changed1 =
-*/
-    DistortedMaster.toDo();
-/*
-    if( changed1 )
-      {
-      for(int i=0; i<mNumChildren; i++)
-        {
-        mChildren.get(i).debug(0);
-        }
-
-      DistortedNode.debugMap();
-      }
-*/
-    // create and delete all underlying OpenGL resources
-    // Watch out: FIRST change topology, only then deal
-    // with OpenGL resources. That's because changing Tree
-    // can result in additional Framebuffers that would need
-    // to be created immediately, before the calls to drawRecursive()
-/*
-    boolean changed2 =
-*/
-    toDo();
-/*
-    if( changed2 )
-      {
-      DistortedObject.debugLists();
-      }
-*/
-    // mark OpenGL state as unknown
-    DistortedRenderState.reset();
-
-    int numRenders=0;
-
-    for(int i=0; i<mNumChildren; i++)
-      {
-      numRenders += mChildren.get(i).renderRecursive(time);
-      }
-
-    setAsOutput(time);
-    numRenders += renderChildren(time,mNumChildren,mChildren);
-
-    if( showDebug ) renderDebug(time);
-
-    return numRenders;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Bind this Surface as a Framebuffer we can render to.
- *
- * @param time Present time, in milliseconds. The point: looking at this param the library can figure
- *             out if this is the first time during present frame that this FBO is being set as output.
- *             If so, the library, in addition to binding the Surface for output, also clears the
- *             Surface's color and depth attachments.
- */
-  public void setAsOutput(long time)
-    {
-    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
-
-    if( mTime!=time )
-      {
-      mTime = time;
-      DistortedRenderState.colorDepthStencilOn();
-      GLES30.glClearColor(mClearR, mClearG, mClearB, mClearA);
-      GLES30.glClearDepthf(mClearDepth);
-      GLES30.glClearStencil(mClearStencil);
-      GLES30.glClear(mClear);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Set mipmap level.
- * <p>
- * Trick for speeding up your renders - one can create a pyramid of OutputSurface objects, each next
- * one some constant FACTOR smaller than the previous (0.5 is the common value), then set the Mipmap
- * Level of the i-th object to be FACTOR^i (we start counting from 0). When rendering any scene into
- * such prepared OutputSurface, the library will make sure to scale any Effects used so that the end
- * scene will end up looking identical no matter which object we render to. Identical, that is, except
- * for the loss of quality and gain in speed associated with rendering to a smaller Surface.
- * <p>
- * Example: if you create two FBOs, one 1000x1000 and another 500x500 in size, and set the second one
- * mipmap to 0.5 (the first one's is 1.0 by default), define Effects to be a single move by (100,100),
- * and render a skinned Mesh into both FBO, the end result will look proportionally the same, because
- * in the second case the move vector (100,100) will be auto-scaled to (50,50).
- *
- * @param mipmap The mipmap level. Acceptable range: 0&lt;mipmap&lt;infinity, although mipmap&gt;1
- *               does not make any sense (that would result in loss of speed and no gain in quality)
- */
-  public void setMipmap(float mipmap)
-    {
-    mMipmap = mipmap;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Set the (R,G,B,A) values of GLES30.glClearColor() to set up color with which to clear
- * this Surface at the beginning of each frame.
- *
- * @param r the Red component. Default: 0.0f
- * @param g the Green component. Default: 0.0f
- * @param b the Blue component. Default: 0.0f
- * @param a the Alpha component. Default: 0.0f
- */
-  public void glClearColor(float r, float g, float b, float a)
-    {
-    mClearR = r;
-    mClearG = g;
-    mClearB = b;
-    mClearA = a;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Uses glClearDepthf() to set up a value with which to clear
- * the Depth buffer of our Surface at the beginning of each frame.
- *
- * @param d the Depth. Default: 1.0f
- */
-  public void glClearDepthf(float d)
-    {
-    mClearDepth = d;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Uses glClearStencil() to set up a value with which to clear the
- * Stencil buffer of our Surface at the beginning of each frame.
- *
- * @param s the Stencil. Default: 0
- */
-  public void glClearStencil(int s)
-    {
-    mClearStencil = s;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Which buffers to Clear at the beginning of each frame?
- * <p>
- * Valid values: 0, or bitwise OR of one or more values from the set GL_COLOR_BUFFER_BIT,
- *               GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT.
- * Default: GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT.
- *
- * @param mask bitwise OR of BUFFER_BITs to clear.
- */
-  public void glClear(int mask)
-    {
-    mClear = mask;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create new Projection matrix.
- *
- * @param fov Vertical 'field of view' of the Projection frustrum (in degrees).
- *            Valid values: 0<=fov<180. FOV==0 means 'parallel projection'.
- * @param near Distance between the screen plane and the near plane.
- *             Valid vaules: 0<near<1. When near==0, the Near Plane is exactly at the tip of the
- *             pyramid. When near==1 (illegal!) the near plane is equivalent to the screen plane.
- */
-  public void setProjection(float fov, float near)
-    {
-    if( fov < 180.0f && fov >=0.0f )
-      {
-      mFOV = fov;
-      }
-
-    if( near<   1.0f && near> 0.0f )
-      {
-      mNear= near;
-      }
-    else if( near<=0.0f )
-      {
-      mNear = 0.01f;
-      }
-    else if( near>=1.0f )
-      {
-      mNear=0.99f;
-      }
-
-    createProjection();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Resize the underlying Framebuffer.
- * <p>
- * This method can be safely called mid-render as it doesn't interfere with rendering.
- *
- * @param width The new width.
- * @param height The new height.
- */
-  public void resize(int width, int height)
-    {
-    if( mWidth!=width || mHeight!=height )
-      {
-      mWidth = width;
-      mHeight= height;
-
-      createProjection();
-
-      if( mColorCreated==CREATED )
-        {
-        markForCreation();
-        recreate();
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Return true if the Surface contains a DEPTH attachment.
- *
- * @return <bold>true</bold> if the Surface contains a DEPTH attachment.
- */
-  public boolean hasDepth()
-    {
-    return mDepthStencilCreated==CREATED;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Return true if the Surface contains a STENCIL attachment.
- *
- * @return <bold>true</bold> if the Surface contains a STENCIL attachment.
- */
-  public boolean hasStencil()
-    {
-    return (mDepthStencilCreated==CREATED && mDepthStencil==BOTH_DEPTH_STENCIL);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Adds a new child to the last position in the list of our Surface's children.
- * <p>
- * We cannot do this mid-render - actual attachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param node The new Node to add.
- */
-  public void attach(DistortedNode node)
-    {
-    mJobs.add(new Job(ATTACH,node,null));
-    DistortedMaster.newSlave(this);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Adds a new child to the last position in the list of our Surface's children.
- * <p>
- * We cannot do this mid-render - actual attachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param surface InputSurface to initialize our child Node with.
- * @param effects DistortedEffects to initialize our child Node with.
- * @param mesh MeshObject to initialize our child Node with.
- * @return the newly constructed child Node, or null if we couldn't allocate resources.
- */
-  public DistortedNode attach(DistortedInputSurface surface, DistortedEffects effects, MeshObject mesh)
-    {
-    DistortedNode node = new DistortedNode(surface,effects,mesh);
-    mJobs.add(new Job(ATTACH,node,null));
-    DistortedMaster.newSlave(this);
-    return node;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes the first occurrence of a specified child from the list of children of our Surface.
- * <p>
- * A bit questionable method as there can be many different Nodes attached as children, some
- * of them having the same Effects but - for instance - different Mesh. Use with care.
- * <p>
- * We cannot do this mid-render - actual detachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param effects DistortedEffects to remove.
- */
-  public void detach(DistortedEffects effects)
-    {
-    long id = effects.getID();
-    DistortedNode node;
-    boolean detached = false;
-
-    for(int i=0; i<mNumChildren; i++)
-      {
-      node = mChildren.get(i);
-
-      if( node.getEffects().getID()==id )
-        {
-        detached = true;
-        mJobs.add(new Job(DETACH,node,null));
-        DistortedMaster.newSlave(this);
-        break;
-        }
-      }
-
-    if( !detached )
-      {
-      // if we failed to detach any, it still might be the case that
-      // there's an ATTACH job that we need to cancel.
-      int num = mJobs.size();
-      Job job;
-
-      for(int i=0; i<num; i++)
-        {
-        job = mJobs.get(i);
-
-        if( job.type==ATTACH && job.node.getEffects()==effects )
-          {
-          mJobs.remove(i);
-          break;
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes the first occurrence of a specified child from the list of children of our Surface.
- * <p>
- * We cannot do this mid-render - actual attachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- *
- * @param node The Node to remove.
- */
-  public void detach(DistortedNode node)
-    {
-    mJobs.add(new Job(DETACH,node,null));
-    DistortedMaster.newSlave(this);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Removes all children Nodes.
- * <p>
- * We cannot do this mid-render - actual attachment will be done just before the next render, by the
- * DistortedMaster (by calling doWork())
- */
-  public void detachAll()
-    {
-    mJobs.add(new Job(DETALL,null,null));
-    DistortedMaster.newSlave(this);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * This is not really part of the public API. Has to be public only because it is a part of the
- * DistortedSlave interface, which should really be a class that we extend here instead but
- * Java has no multiple inheritance.
- *
- * @y.exclude
- */
-  public void doWork()
-    {
-    int num = mJobs.size();
-    Job job;
-
-    for(int i=0; i<num; i++)
-      {
-      job = mJobs.remove(0);
-
-      switch(job.type)
-        {
-        case ATTACH: if( mChildren==null ) mChildren = new ArrayList<>(2);
-                     job.node.setSurfaceParent(this);
-                     DistortedMaster.addSorted(mChildren,job.node);
-                     mNumChildren++;
-                     break;
-        case DETACH: if( mNumChildren>0 && mChildren.remove(job.node) )
-                       {
-                       job.node.setSurfaceParent(null);
-                       mNumChildren--;
-                       }
-                     break;
-        case DETALL: if( mNumChildren>0 )
-                       {
-                       DistortedNode tmp;
-
-                       for(int j=mNumChildren-1; j>=0; j--)
-                         {
-                         tmp = mChildren.remove(j);
-                         tmp.setSurfaceParent(null);
-                         }
-
-                       mNumChildren = 0;
-                       }
-                     break;
-        case SORT  : job.node.setPost(job.dep);
-                     mChildren.remove(job.node);
-                     DistortedMaster.addSorted(mChildren,job.node);
-                     break;
-        }
-      }
-    }
-}
diff --git a/src/main/java/org/distorted/library/DistortedRenderState.java b/src/main/java/org/distorted/library/DistortedRenderState.java
deleted file mode 100644
index 4d6639d..0000000
--- a/src/main/java/org/distorted/library/DistortedRenderState.java
+++ /dev/null
@@ -1,570 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import android.opengl.GLES30;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Remember the OpenGL state.
- * <p>
- * This is a member of DistortedNode. Remembers the OpenGL state we want to set just before rendering
- * the Node.
- */
-public class DistortedRenderState
-{
-  // TODO: figure this out dynamically; this assumes 8 bit stencil buffer.
-  private static final int STENCIL_MASK = (1<<8)-1;
-
-  private static class RenderState
-    {
-    private int colorMaskR, colorMaskG, colorMaskB, colorMaskA;
-    private int depthMask;
-    private int stencilMask;
-    private int depthTest;
-    private int stencilTest;
-    private int stencilFuncFunc, stencilFuncRef, stencilFuncMask;
-    private int stencilOpSfail, stencilOpDpfail, stencilOpDppass;
-    private int depthFunc;
-    private int blend;
-    private int blendSrc, blendDst;
-    }
-
-  private RenderState mState;          // state the current object wants to have
-  static private RenderState cState = new RenderState();   // current OpenGL Stave
-  static private RenderState sState = new RenderState();   // saved OpenGL state
-
-  private int mClear;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// default: color writes on, depth test and writes on, blending on, stencil off.
-
-  DistortedRenderState()
-    {
-    mState = new RenderState();
-
-    mState.colorMaskR = 1;
-    mState.colorMaskG = 1;
-    mState.colorMaskB = 1;
-    mState.colorMaskA = 1;
-
-    mState.depthTest  = 1;
-    mState.depthMask  = 1;
-    mState.depthFunc  = GLES30.GL_LEQUAL;
-
-    mState.blend      = 1;
-    mState.blendSrc   = GLES30.GL_SRC_ALPHA;
-    mState.blendDst   = GLES30.GL_ONE_MINUS_SRC_ALPHA;
-
-    mState.stencilTest     = 0;
-    mState.stencilMask     = STENCIL_MASK;
-    mState.stencilFuncFunc = GLES30.GL_NEVER;
-    mState.stencilFuncRef  = 0;
-    mState.stencilFuncMask = STENCIL_MASK;
-    mState.stencilOpSfail  = GLES30.GL_KEEP;
-    mState.stencilOpDpfail = GLES30.GL_KEEP;
-    mState.stencilOpDppass = GLES30.GL_KEEP;
-
-    mClear = 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// reset state of everything to 'unknown'
-
-  static void reset()
-    {
-    cState.colorMaskR = -1;
-    cState.colorMaskG = -1;
-    cState.colorMaskB = -1;
-    cState.colorMaskA = -1;
-
-    cState.depthTest  = -1;
-    cState.depthMask  = -1;
-    cState.depthFunc  = -1;
-
-    cState.blend      = -1;
-    cState.blendSrc   = -1;
-    cState.blendDst   = -1;
-
-    cState.stencilTest     = -1;
-    cState.stencilMask     = -1;
-    cState.stencilFuncFunc = -1;
-    cState.stencilFuncRef  = -1;
-    cState.stencilFuncMask = -1;
-    cState.stencilOpSfail  = -1;
-    cState.stencilOpDpfail = -1;
-    cState.stencilOpDppass = -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void colorDepthStencilOn()
-    {
-    if( cState.colorMaskR!=1 || cState.colorMaskG!=1 || cState.colorMaskB!=1 || cState.colorMaskA!=1 )
-      {
-      sState.colorMaskR = cState.colorMaskR;
-      sState.colorMaskG = cState.colorMaskG;
-      sState.colorMaskB = cState.colorMaskB;
-      sState.colorMaskA = cState.colorMaskA;
-      cState.colorMaskR = 1;
-      cState.colorMaskG = 1;
-      cState.colorMaskB = 1;
-      cState.colorMaskA = 1;
-      GLES30.glColorMask(true,true,true,true);
-      }
-    if( cState.depthMask!=1 )
-      {
-      sState.depthMask = cState.depthMask;
-      cState.depthMask = 1;
-      GLES30.glDepthMask(true);
-      }
-    if( cState.stencilMask!= STENCIL_MASK )
-      {
-      sState.stencilMask = cState.stencilMask;
-      cState.stencilMask = STENCIL_MASK;
-      GLES30.glStencilMask(cState.stencilMask);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void switchOffDrawing()
-    {
-    GLES30.glEnable(GLES30.GL_SCISSOR_TEST);
-    GLES30.glScissor(0,0,0,0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void restoreDrawing()
-    {
-    GLES30.glDisable(GLES30.GL_SCISSOR_TEST);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void setUpStencilMark()
-    {
-    if( cState.stencilTest!=1 )
-      {
-      sState.stencilTest = cState.stencilTest;
-      cState.stencilTest = 1;
-      //android.util.Log.d("State", "stencil test on");
-      GLES30.glEnable(GLES30.GL_STENCIL_TEST);
-      }
-    if( cState.stencilFuncFunc!=GLES30.GL_ALWAYS || cState.stencilFuncRef!=1 || cState.stencilFuncMask!=STENCIL_MASK )
-      {
-      sState.stencilFuncFunc = cState.stencilFuncFunc;
-      sState.stencilFuncRef  = cState.stencilFuncRef;
-      sState.stencilFuncMask = cState.stencilFuncMask;
-      cState.stencilFuncFunc = GLES30.GL_ALWAYS;
-      cState.stencilFuncRef  = 1;
-      cState.stencilFuncMask = STENCIL_MASK;
-      //android.util.Log.d("State", "stencil func on");
-      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
-      }
-    if( cState.stencilOpSfail!=GLES30.GL_KEEP || cState.stencilOpDpfail!=GLES30.GL_KEEP || cState.stencilOpDppass!=GLES30.GL_REPLACE )
-      {
-      sState.stencilOpSfail = cState.stencilOpSfail;
-      sState.stencilOpDpfail= cState.stencilOpDpfail;
-      sState.stencilOpDppass= cState.stencilOpDppass;
-      cState.stencilOpSfail = GLES30.GL_KEEP;
-      cState.stencilOpDpfail= GLES30.GL_KEEP;
-      cState.stencilOpDppass= GLES30.GL_REPLACE;
-      //android.util.Log.d("State", "stencil op on");
-      GLES30.glStencilOp(cState.stencilOpSfail,cState.stencilOpDpfail,cState.stencilOpDppass);
-      }
-    if( cState.colorMaskR!=0 || cState.colorMaskG!=0 || cState.colorMaskB!=0 || cState.colorMaskA!=0 )
-      {
-      sState.colorMaskR = cState.colorMaskR;
-      sState.colorMaskG = cState.colorMaskG;
-      sState.colorMaskB = cState.colorMaskB;
-      sState.colorMaskA = cState.colorMaskA;
-      cState.colorMaskR = 0;
-      cState.colorMaskG = 0;
-      cState.colorMaskB = 0;
-      cState.colorMaskA = 0;
-      //android.util.Log.d("State", "switch off color writing");
-      GLES30.glColorMask(false,false,false,false);
-      }
-    if( cState.depthMask!=1 )
-      {
-      sState.depthMask = cState.depthMask;
-      cState.depthMask = 1;
-      //android.util.Log.d("State", "switch on depth writing");
-      GLES30.glDepthMask(true);
-      }
-    if( cState.stencilMask!= STENCIL_MASK )
-      {
-      sState.stencilMask = cState.stencilMask;
-      cState.stencilMask = STENCIL_MASK;
-      //android.util.Log.d("State", "stencil mask on");
-      GLES30.glStencilMask(cState.stencilMask);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void unsetUpStencilMark()
-    {
-    if( sState.stencilTest!=cState.stencilTest )
-      {
-      cState.stencilTest = sState.stencilTest;
-
-      if (cState.stencilTest == 0)
-        {
-        GLES30.glDisable(GLES30.GL_STENCIL_TEST);
-        }
-      else
-        {
-        GLES30.glEnable(GLES30.GL_STENCIL_TEST);
-        }
-      }
-    if( sState.colorMaskR!=cState.colorMaskR || sState.colorMaskG!=cState.colorMaskG || sState.colorMaskB!=cState.colorMaskB || sState.colorMaskA!=cState.colorMaskA)
-      {
-      cState.colorMaskR = sState.colorMaskR;
-      cState.colorMaskG = sState.colorMaskG;
-      cState.colorMaskB = sState.colorMaskB;
-      cState.colorMaskA = sState.colorMaskA;
-      GLES30.glColorMask(cState.colorMaskR==1,cState.colorMaskG==1,cState.colorMaskB==1,cState.colorMaskA==1);
-      }
-    if( sState.depthMask!=cState.depthMask )
-      {
-      cState.depthMask = sState.depthMask;
-      GLES30.glDepthMask(cState.depthMask==1);
-      }
-    if( sState.stencilMask!=cState.stencilMask )
-      {
-      cState.stencilMask = sState.stencilMask;
-      GLES30.glStencilMask(cState.stencilMask);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void useStencilMark()
-    {
-    if( cState.stencilTest!=1 )
-      {
-      sState.stencilTest = cState.stencilTest;
-      cState.stencilTest = 1;
-      GLES30.glEnable(GLES30.GL_STENCIL_TEST);
-      }
-    if( cState.stencilFuncFunc!=GLES30.GL_EQUAL || cState.stencilFuncRef!=1 || cState.stencilFuncMask!=STENCIL_MASK )
-      {
-      sState.stencilFuncFunc = cState.stencilFuncFunc;
-      sState.stencilFuncRef  = cState.stencilFuncRef;
-      sState.stencilFuncMask = cState.stencilFuncMask;
-      cState.stencilFuncFunc = GLES30.GL_EQUAL;
-      cState.stencilFuncRef  = 1;
-      cState.stencilFuncMask = STENCIL_MASK;
-      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
-      }
-    if( cState.stencilMask!= 0x00 )
-      {
-      sState.stencilMask = cState.stencilMask;
-      cState.stencilMask = 0x00;
-      GLES30.glStencilMask(cState.stencilMask);
-      }
-     if( cState.depthMask!=0 )
-      {
-      sState.depthMask = cState.depthMask;
-      cState.depthMask = 0;
-      GLES30.glDepthMask(false);
-      }
-    if( cState.depthTest!=0 )
-      {
-      sState.depthTest = cState.depthTest;
-      cState.depthTest = 0;
-      GLES30.glDisable(GLES30.GL_DEPTH_TEST);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void unuseStencilMark()
-    {
-    if( sState.stencilTest!=cState.stencilTest )
-      {
-      cState.stencilTest = sState.stencilTest;
-
-      if (cState.stencilTest == 0)
-        {
-        GLES30.glDisable(GLES30.GL_STENCIL_TEST);
-        }
-      else
-        {
-        GLES30.glEnable(GLES30.GL_STENCIL_TEST);
-        }
-      }
-    if( sState.stencilFuncFunc!=cState.stencilFuncFunc || sState.stencilFuncRef!=cState.stencilFuncRef || sState.stencilFuncMask!=cState.stencilFuncMask )
-      {
-      cState.stencilFuncFunc = sState.stencilFuncFunc;
-      cState.stencilFuncRef  = sState.stencilFuncRef ;
-      cState.stencilFuncMask = sState.stencilFuncMask;
-      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
-      }
-    if( sState.stencilMask!=cState.stencilMask )
-      {
-      cState.stencilMask = sState.stencilMask;
-      GLES30.glStencilMask(cState.stencilMask);
-      }
-    if( sState.depthMask!=cState.depthMask )
-      {
-      cState.depthMask = sState.depthMask;
-      GLES30.glDepthMask(cState.depthMask==1);
-      }
-    if( sState.depthTest!=cState.depthTest )
-      {
-      cState.depthTest = sState.depthTest;
-
-      if (cState.depthTest == 0)
-        {
-        GLES30.glDisable(GLES30.GL_DEPTH_TEST);
-        }
-      else
-        {
-        GLES30.glEnable(GLES30.GL_DEPTH_TEST);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void apply()
-    {
-    //android.util.Log.e("State", "APPLYING STATE");
-
-    /////////////////////////////////////////////////////
-    // 1. Write to color buffer?
-    if( mState.colorMaskR!=cState.colorMaskR || mState.colorMaskG!=cState.colorMaskG || mState.colorMaskB!=cState.colorMaskB || mState.colorMaskA!=cState.colorMaskA)
-      {
-      //android.util.Log.d("State", "setting color mask");
-      cState.colorMaskR = mState.colorMaskR;
-      cState.colorMaskG = mState.colorMaskG;
-      cState.colorMaskB = mState.colorMaskB;
-      cState.colorMaskA = mState.colorMaskA;
-      GLES30.glColorMask(cState.colorMaskR==1,cState.colorMaskG==1,cState.colorMaskB==1,cState.colorMaskA==1);
-      }
-
-    /////////////////////////////////////////////////////
-    // 2. Enable Depth test?
-    if( mState.depthTest!=cState.depthTest )
-      {
-      cState.depthTest = mState.depthTest;
-
-      if (cState.depthTest == 0)
-        {
-        //android.util.Log.d("State", "disabling depth test");
-        GLES30.glDisable(GLES30.GL_DEPTH_TEST);
-        }
-      else
-        {
-        //android.util.Log.d("State", "enable depth test");
-        GLES30.glEnable(GLES30.GL_DEPTH_TEST);
-        }
-      }
-
-    /////////////////////////////////////////////////////
-    // 3. Change Depth Function?
-    if( mState.depthFunc!=cState.depthFunc )
-      {
-      //android.util.Log.d("State", "setting depth func");
-      cState.depthFunc = mState.depthFunc;
-      GLES30.glDepthFunc(cState.depthFunc);
-      }
-
-    /////////////////////////////////////////////////////
-    // 4. Write to Depth buffer?
-    if( mState.depthMask!=cState.depthMask )
-      {
-      //android.util.Log.d("State", "setting depth mask");
-      cState.depthMask = mState.depthMask;
-      GLES30.glDepthMask(cState.depthMask==1);
-      }
-
-    /////////////////////////////////////////////////////
-    // 5. Enable Blending?
-    if( mState.blend!=cState.blend )
-      {
-      cState.blend = mState.blend;
-
-      if (cState.blend == 0)
-        {
-        //android.util.Log.d("State", "disabling blending");
-        GLES30.glDisable(GLES30.GL_BLEND);
-        }
-      else
-        {
-        //android.util.Log.d("State", "enabling blending");
-        GLES30.glEnable(GLES30.GL_BLEND);
-        }
-      }
-
-    /////////////////////////////////////////////////////
-    // 6. Change Blend function?
-    if( mState.blendSrc!=cState.blendSrc || mState.blendDst!=cState.blendDst )
-      {
-      //android.util.Log.d("State", "setting blend function");
-      cState.blendSrc = mState.blendSrc;
-      cState.blendDst = mState.blendDst;
-      GLES30.glBlendFunc(cState.blendSrc,cState.blendDst);
-      }
-
-    /////////////////////////////////////////////////////
-    // 7. Enable/Disable Stencil Test?
-    if( mState.stencilTest!=cState.stencilTest )
-      {
-      cState.stencilTest = mState.stencilTest;
-
-      if (cState.stencilTest == 0)
-        {
-        //android.util.Log.d("State", "disabling stencil test");
-        GLES30.glDisable(GLES30.GL_STENCIL_TEST);
-        }
-      else
-        {
-        //android.util.Log.d("State", "enabling stencil test");
-        GLES30.glEnable(GLES30.GL_STENCIL_TEST);
-        }
-      }
-
-    /////////////////////////////////////////////////////
-    // 8. Adjust Stencil function?
-    if( mState.stencilFuncFunc!=cState.stencilFuncFunc || mState.stencilFuncRef!=cState.stencilFuncRef || mState.stencilFuncMask!=cState.stencilFuncMask )
-      {
-      //android.util.Log.d("State", "setting stencil function");
-      cState.stencilFuncFunc = mState.stencilFuncFunc;
-      cState.stencilFuncRef  = mState.stencilFuncRef ;
-      cState.stencilFuncMask = mState.stencilFuncMask;
-      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
-      }
-
-    /////////////////////////////////////////////////////
-    // 9. Adjust Stencil operation?
-    if( mState.stencilOpSfail!=cState.stencilOpSfail || mState.stencilOpDpfail!=cState.stencilOpDpfail || mState.stencilOpDppass!=cState.stencilOpDppass )
-      {
-      //android.util.Log.d("State", "setting stencil op");
-      cState.stencilOpSfail = mState.stencilOpSfail;
-      cState.stencilOpDpfail= mState.stencilOpDpfail;
-      cState.stencilOpDppass= mState.stencilOpDppass;
-      GLES30.glStencilOp(cState.stencilOpSfail,cState.stencilOpDpfail,cState.stencilOpDppass);
-      }
-
-    /////////////////////////////////////////////////////
-    // 10. Write to Stencil buffer?
-    if( mState.stencilMask!=cState.stencilMask )
-      {
-      //android.util.Log.d("State", "setting stencil mask");
-      cState.stencilMask = mState.stencilMask;
-      GLES30.glStencilMask(cState.stencilMask);
-      }
-
-    /////////////////////////////////////////////////////
-    // 11. Clear buffers?
-    if( mClear!=0 )
-      {
-      //android.util.Log.d("State", "clearing buffer");
-      GLES30.glClear(mClear);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glColorMask(boolean r, boolean g, boolean b, boolean a)
-    {
-    mState.colorMaskR = (r ? 1:0);
-    mState.colorMaskG = (g ? 1:0);
-    mState.colorMaskB = (b ? 1:0);
-    mState.colorMaskA = (a ? 1:0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glDepthMask(boolean mask)
-    {
-    mState.depthMask = (mask ? 1:0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glStencilMask(int mask)
-    {
-    mState.stencilMask = mask;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glEnable(int test)
-    {
-         if( test==GLES30.GL_DEPTH_TEST   ) mState.depthTest   = 1;
-    else if( test==GLES30.GL_STENCIL_TEST ) mState.stencilTest = 1;
-    else if( test==GLES30.GL_BLEND        ) mState.blend       = 1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glDisable(int test)
-    {
-         if( test==GLES30.GL_DEPTH_TEST   ) mState.depthTest   = 0;
-    else if( test==GLES30.GL_STENCIL_TEST ) mState.stencilTest = 0;
-    else if( test==GLES30.GL_BLEND        ) mState.blend       = 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glStencilFunc(int func, int ref, int mask)
-    {
-    mState.stencilFuncFunc = func;
-    mState.stencilFuncRef  = ref;
-    mState.stencilFuncMask = mask;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glStencilOp(int sfail, int dpfail, int dppass)
-    {
-    mState.stencilOpSfail = sfail;
-    mState.stencilOpDpfail= dpfail;
-    mState.stencilOpDppass= dppass;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glDepthFunc(int func)
-    {
-    mState.depthFunc = func;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glBlendFunc(int src, int dst)
-    {
-    mState.blendSrc = src;
-    mState.blendDst = dst;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void glClear(int mask)
-    {
-    mClear = mask;
-    }
-}
diff --git a/src/main/java/org/distorted/library/DistortedScreen.java b/src/main/java/org/distorted/library/DistortedScreen.java
deleted file mode 100644
index 9cf4768..0000000
--- a/src/main/java/org/distorted/library/DistortedScreen.java
+++ /dev/null
@@ -1,146 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ConfigurationInfo;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.opengl.GLES30;
-import android.opengl.GLSurfaceView;
-
-import org.distorted.library.type.Static3D;
-
-/**
- * Class which represents the Screen.
- * <p>
- * User is able to render to it just like to a DistortedFramebuffer.
- */
-public class DistortedScreen extends DistortedOutputSurface
-  {
-  ///// DEBUGGING ONLY /////////////////////////
-  private static final int NUM_FRAMES  = 100;
-
-  private MeshObject fpsMesh;
-  private DistortedTexture fpsTexture;
-  private DistortedEffects fpsEffects;
-  private Canvas fpsCanvas;
-  private Bitmap fpsBitmap;
-  private Paint mPaint;
-  private int fpsH, fpsW;
-  private String fpsString = "";
-  private long lastTime=0;
-  private long[] durations;
-  private int currDuration;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// here we don't manage underlying OpenGL assets ourselves
-
-  void create()   {}
-  void delete()   {}
-  void recreate() {}
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void prepareDebug(long time)
-    {
-    if( lastTime==0 ) lastTime = time;
-
-    currDuration++;
-    if (currDuration >= NUM_FRAMES) currDuration = 0;
-    durations[NUM_FRAMES] += ((time - lastTime) - durations[currDuration]);
-    durations[currDuration] = time - lastTime;
-
-    fpsString = "" + ((int)(10000.0f*NUM_FRAMES/durations[NUM_FRAMES]))/10.0f;
-
-    mPaint.setColor(0xffffffff);
-    fpsCanvas.drawRect(0, 0, fpsW, fpsH, mPaint);
-    mPaint.setColor(0xff000000);
-    fpsCanvas.drawText(fpsString, fpsW/2, 0.75f*fpsH, mPaint);
-    fpsTexture.setTexture(fpsBitmap);
-
-    lastTime = time;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void renderDebug(long time)
-    {
-    if (fpsTexture.setAsInput())
-      {
-      setAsOutput(time);
-      GLES30.glColorMask(true,true,true,true);
-      GLES30.glDepthMask(false);
-      GLES30.glDisable(GLES30.GL_STENCIL_TEST);
-      GLES30.glDisable(GLES30.GL_DEPTH_TEST);
-      fpsEffects.drawPriv(fpsW/2.0f, fpsH/2.0f, fpsMesh, this, time, 0);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create a new Screen.
- * <p>
- * Has to be followed by a 'resize()' to set the size.
- */
-  public DistortedScreen(GLSurfaceView view)
-    {
-    // set color to 'DONT_CREATE' so that Screens will not get added to the Surface lists
-    // set depth to 'CREATED' so that depth will be, by default, on.
-    super(0,0,DONT_CREATE,1,DEPTH_NO_STENCIL,0,TYPE_USER);
-
-    if( view!=null )
-      {
-      Context context = view.getContext();
-      final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-      final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
-      view.setEGLContextClientVersion((configurationInfo.reqGlEsVersion >> 16) >= 3 ? 3 : 2);
-      }
-
-    /////// DEBUGGING ONLY //////////////
-    fpsW = 120;
-    fpsH =  70;
-
-    fpsBitmap = Bitmap.createBitmap(fpsW,fpsH, Bitmap.Config.ARGB_8888);
-    fpsMesh = new MeshFlat(1,1);
-    fpsTexture = new DistortedTexture(fpsW,fpsH);
-    fpsTexture.setTexture(fpsBitmap);
-    fpsCanvas = new Canvas(fpsBitmap);
-    fpsEffects = new DistortedEffects();
-    fpsEffects.move( new Static3D(5,5,0) );
-
-    mPaint = new Paint();
-    mPaint.setAntiAlias(true);
-    mPaint.setTextAlign(Paint.Align.CENTER);
-    mPaint.setTextSize(0.7f*fpsH);
-
-    durations = new long[NUM_FRAMES+1];
-    currDuration = 0;
-
-    for(int i=0; i<NUM_FRAMES+1; i++) durations[i]=16;  // Assume FPS will be
-    durations[NUM_FRAMES] = NUM_FRAMES*16;              // close to 1000/16 ~ 60
-    }
-  }
diff --git a/src/main/java/org/distorted/library/DistortedSlave.java b/src/main/java/org/distorted/library/DistortedSlave.java
deleted file mode 100644
index 1428d06..0000000
--- a/src/main/java/org/distorted/library/DistortedSlave.java
+++ /dev/null
@@ -1,37 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Package-private interface implemented by all objects which can be assigned work by the DistortedMaster.
- * <p>
- * All the methods below are really meant to be package-local; and this interface should really be a
- * package-local class which other classes would extend (but that's impossible because OutputSurface
- * already extends other class).
- */
-interface DistortedSlave
-  {
-  /**
-   * Not part of public API, do not document
-   * @y.exclude
-   */
-  void doWork();
-  }
diff --git a/src/main/java/org/distorted/library/DistortedSurface.java b/src/main/java/org/distorted/library/DistortedSurface.java
deleted file mode 100644
index 0aa0f23..0000000
--- a/src/main/java/org/distorted/library/DistortedSurface.java
+++ /dev/null
@@ -1,95 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class DistortedSurface extends DistortedObject
-{
-  int mColorCreated;
-  int mNumColors;
-  int[] mColorH;
-  int mWidth, mHeight;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  DistortedSurface(int width, int height, int create, int numcolors, int type)
-    {
-    super(create,type);
-
-    mNumColors    = numcolors;
-    mWidth        = width ;
-    mHeight       = height;
-    mColorCreated = create;
-
-    if( mNumColors>0 )
-      {
-      mColorH = new int[mNumColors];
-      for( int i=0; i<mNumColors; i++ )  mColorH[i] = 0;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// debugging only
-
-  String printDetails()
-    {
-    return getClass().getSimpleName()+" "+mWidth+"x"+mHeight;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Return the width of this Surface.
- *
- * @return width of the Object, in pixels.
- */
-  public int getWidth()
-    {
-    return mWidth;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Return the height of this Surface.
- *
- * @return height of the Object, in pixels.
- */
-  public int getHeight()
-    {
-    return mHeight;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Return the depth of this Surface.
- * <p>
- * Admittedly quite a strange method. Why do we need to pass a Mesh to it? Because one cannot determine
- * 'depth' of a Surface (bitmap really!) when rendered based only on the texture itself, that depends
- * on the Mesh it is rendered with.
- *
- * @return depth of the Object, in pixels.
- */
-  public int getDepth(MeshObject mesh)
-    {
-    return mesh==null ? 0 : (int)(mWidth*mesh.zFactor);
-    }
-}
diff --git a/src/main/java/org/distorted/library/DistortedTexture.java b/src/main/java/org/distorted/library/DistortedTexture.java
deleted file mode 100644
index fa4b903..0000000
--- a/src/main/java/org/distorted/library/DistortedTexture.java
+++ /dev/null
@@ -1,183 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Matrix;
-import android.graphics.Paint;
-import android.opengl.GLES30;
-import android.opengl.GLUtils;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Class which represents a OpenGL Texture object.
- * <p>
- * Create a Texture of arbitrary size and feed some pixels to it via the setTexture() method.
- */
-public class DistortedTexture extends DistortedSurface implements DistortedInputSurface
-  {
-  private Bitmap mBmp= null;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// We have to flip vertically every single Bitmap that we get fed with.
-//
-// Reason: textures read from files are the only objects in OpenGL which have their origins at the
-// upper-left corner. Everywhere else the origin is in the lower-left corner. Thus we have to flip.
-// The alternative solution, namely inverting the y-coordinate of the TexCoord does not really work-
-// i.e. it works only in case of rendering directly to the screen, but if we render to an FBO and
-// then take the FBO and render to screen, (DistortedNode does so!) things get inverted as textures
-// created from FBO have their origins in the lower-left... Mindfuck!
-
-  private static Bitmap flipBitmap(Bitmap src)
-    {
-    Matrix matrix = new Matrix();
-    matrix.preScale(1.0f,-1.0f);
-
-    return Bitmap.createBitmap(src,0,0,src.getWidth(),src.getHeight(), matrix,true);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// must be called from a thread holding OpenGL Context
-
-  void create()
-    {
-    if( mBmp!=null )
-      {
-      if( mColorCreated==NOT_CREATED_YET )
-        {
-        mColorCreated = CREATED;
-        GLES30.glGenTextures(1, mColorH, 0);
-        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
-        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR );
-        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR );
-        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE );
-        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE );
-        GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, flipBitmap(mBmp), 0);
-        }
-      else
-        {
-        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
-        GLUtils.texSubImage2D(GLES30.GL_TEXTURE_2D, 0,0,0,flipBitmap(mBmp));
-        }
-
-      mBmp = null;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// must be called from a thread holding OpenGL Context
-
-  void delete()
-    {
-    if( mColorH[0]>0 )
-      {
-      GLES30.glDeleteTextures(1, mColorH, 0);
-      mColorH[0] = 0;
-      mColorCreated = NOT_CREATED_YET;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// called from onDestroy(); mark OpenGL assets as 'not created'
-
-  void recreate()
-    {
-    if( mColorCreated!=DONT_CREATE )
-      {
-      mColorCreated = NOT_CREATED_YET;
-      mColorH[0] = 0;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// create SYSTEM or TREE textures (those are just like normal Textures, just hold information
-// that they were autocreated only for the Library's internal purposes (SYSTEM) or for using
-// inside a Tree of DistortedNodes (TREE)
-// SYSTEM surfaces do not get removed in onDestroy().
-
-  public DistortedTexture(int width, int height, int type)
-    {
-    super(width,height,NOT_CREATED_YET,1,type);
-    mBmp= null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create empty texture of given dimensions.
- */
-  public DistortedTexture(int width, int height)
-    {
-    super(width,height,NOT_CREATED_YET,1,TYPE_USER);
-    mBmp= null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Bind the underlying rectangle of pixels as a OpenGL Texture.
- */
-  public boolean setAsInput()
-    {
-    if( mColorH[0]>0 )
-      {
-      GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
-      return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Sets the underlying android.graphics.Bitmap object.
- * <p>
- * You can only recycle() the passed Bitmap once the OpenGL context gets created (i.e. after call
- * to GLSurfaceView.onSurfaceCreated) because only after this point can the Library upload it to the GPU!
- *
- * @param bmp The android.graphics.Bitmap object to apply effects to and display.
- */
-  public void setTexture(Bitmap bmp)
-    {
-    mBmp= bmp;
-    markForCreation();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Paints the Texture with solid color.
- *
- * @param argb The color to paint the Texture with.
- */
-  public void setColor(int argb)
-    {
-    Paint paint = new Paint();
-    paint.setColor(argb);
-    paint.setStyle(Paint.Style.FILL);
-
-    mBmp = Bitmap.createBitmap(1,1, Bitmap.Config.ARGB_8888);
-    Canvas canvas = new Canvas(mBmp);
-    canvas.drawRect(0,0,1,1,paint);
-
-    markForCreation();
-    }
-  }
diff --git a/src/main/java/org/distorted/library/EffectQuality.java b/src/main/java/org/distorted/library/EffectQuality.java
deleted file mode 100644
index 2279294..0000000
--- a/src/main/java/org/distorted/library/EffectQuality.java
+++ /dev/null
@@ -1,50 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * A list of quality levels.
- * <p>
- * One can set quality of a Postprocessing Effect to one of those. The lower the quality, the faster
- * the rendering will be.
- *
- * @see DistortedEffectsPostprocess
- */
-public enum EffectQuality
-  {
-  HIGHEST  ( 0 ),   // has to start from 0
-  HIGH     ( 1 ),
-  MEDIUM   ( 2 ),
-  LOW      ( 3 );
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  final static float MULTIPLIER = 0.5f;      // each next Quality level renders into 1/0.5 smaller buffers
-  final static int LENGTH = values().length;
-
-  final int level;
-
-  EffectQuality(int level)
-    {
-    this.level = level;
-    }
-  }
-
diff --git a/src/main/java/org/distorted/library/MeshCubes.java b/src/main/java/org/distorted/library/MeshCubes.java
deleted file mode 100644
index dd56f8f..0000000
--- a/src/main/java/org/distorted/library/MeshCubes.java
+++ /dev/null
@@ -1,764 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create a 3D grid composed of Cubes.
- * <p>
- * Any subset of a MxNx1 cuboid is possible.
- */
-public class MeshCubes extends MeshObject
-   {
-   private static final float R = 0.0f;//0.2f;
-
-   private static final int NORTH = 0;
-   private static final int WEST  = 1;
-   private static final int EAST  = 2;
-   private static final int SOUTH = 3;
-
-   private static final float[] mNormalX = new float[4];
-   private static final float[] mNormalY = new float[4];
-   private static final float[] mNormalZ = new float[4];
-
-   private class Edge
-     {
-     final int side; 
-     final int row;
-     final int col;
-     
-     Edge(int s, int r, int c)
-       {
-       side= s; 
-       row = r;
-       col = c;
-       }
-     }
-   
-   private int mCols, mRows, mSlices;
-   private int[][] mCubes;
-   private ArrayList<Edge> mEdges = new ArrayList<>();
-
-   private int remainingVert;
-   private int mSideBends;
-   private int mEdgeNum;
-   private int mSideWalls;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// a Block is split into two triangles along the NE-SW line iff it is in the top-right
-// or bottom-left quadrant of the grid.
-
-   private boolean isNE(int row,int col)
-     {
-     return ( (2*row<mRows)^(2*col<mCols) );
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// return the number of vertices our grid will contain
-
-   private int computeDataLength()
-      {
-      int frontWalls=0, frontSegments=0, triangleShifts=0, windingShifts=0;
-      int shiftCol = (mCols-1)/2;
-
-      boolean lastBlockIsNE=false;
-      boolean thisBlockIsNE;        // the block we are currently looking at is split into
-                                    // two triangles along the NE-SW line (rather than NW-SE)
-      for(int row=0; row<mRows; row++)
-        {
-        if( mCols>=2 && (mCubes[row][shiftCol]%2 == 1) && (mCubes[row][shiftCol+1]%2 == 1) ) triangleShifts++;
-
-        for(int col=0; col<mCols; col++)
-          {
-          if( mCubes[row][col]%2 == 1 )  // land
-            {
-            thisBlockIsNE = isNE(row,col);
-            if( thisBlockIsNE^lastBlockIsNE ) windingShifts++;
-            lastBlockIsNE = thisBlockIsNE;
-            frontWalls++;
-            if( col==mCols-1 || mCubes[row][col+1]%2 == 0 ) frontSegments++;
-            }
-          }
-        }
-
-      int frontVert       = 2*( frontWalls + 2*frontSegments - 1) +2*triangleShifts + windingShifts;
-      int sideVertOneSlice= 2*( mSideWalls + mSideBends + mEdgeNum -1);
-      int sideVert        = 2*(mSlices-1) + mSlices*sideVertOneSlice;
-      int firstWinding    = (mSlices>0 && (frontVert+1)%2==1 ) ? 1:0;
-      int dataL           = mSlices==0 ? frontVert : (frontVert+1) +firstWinding+ (1+sideVert+1) + (1+frontVert);
-/*
-      android.util.Log.e("CUBES","triangleShifts="+triangleShifts+" windingShifts="+windingShifts+" winding1="+firstWinding+" frontVert="+frontVert+" sideVert="+sideVert);
-      android.util.Log.e("CUBES", "frontW="+frontWalls+" fSegments="+frontSegments+" sWalls="+mSideWalls+" sSegments="+mEdgeNum+" sideBends="+mSideBends+" dataLen="+dataL );
-*/
-      return dataL<0 ? 0:dataL;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/*
-   private static String debug(short[] val)
-     {
-     String ret="";j
-     
-     for(int i=0; i<val.length; i++) ret+=(" "+val[i]); 
-     
-     return ret;
-     }
-*/
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/*
-   private static String debug(float[] val, int stop)
-     {
-     String ret="";
-
-     for(int i=0; i<val.length; i++) 
-        {
-        if( i%stop==0 ) ret+="\n";
-        ret+=(" "+val[i]);
-        }
-
-     return ret;
-     }
-*/
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/*
-   private static String debug(Edge e)
-     {
-     String d = "";
-     
-     switch(e.side)
-       {
-       case NORTH: d+="NORTH "; break;
-       case SOUTH: d+="SOUTH "; break;
-       case WEST : d+="WEST  "; break;
-       case EAST : d+="EAST  "; break;
-       }
-     
-     d+=("("+e.row+","+e.col+")");
-     
-     return d;
-     }   
-*/
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   private void prepareDataStructures(int cols, String desc, int slices)
-     {
-     mRows      =0;
-     mCols      =0;
-     mSlices    =slices;
-     numVertices=0;
-     
-     if( cols>0 && desc.contains("1") )
-       {
-       mCols = cols;
-       mRows = desc.length()/cols;
-
-       mCubes = new int[mRows][mCols];
-       
-       for(int j=0; j<mCols; j++)
-         for(int i=0; i<mRows; i++)
-           mCubes[i][j] = (desc.charAt(i*mCols+j) == '1' ? 1:0);
-
-       markRegions();
-       numVertices = computeDataLength();
-
-       remainingVert = numVertices;
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// full grid
-
-   private void prepareDataStructures(int cols, int rows, int slices)
-     {
-     mRows       =rows;
-     mCols       =cols;
-     mSlices     =slices;
-     numVertices =   0;
-
-     if( cols>0 && rows>0 )
-       {
-       mCubes = new int[mRows][mCols];
-
-       for(int j=0; j<mCols; j++)
-         for(int i=0; i<mRows; i++)
-           mCubes[i][j] = 1;
-
-       markRegions();
-       numVertices = computeDataLength();
-
-       remainingVert = numVertices;
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Mark all the 'regions' of our grid  - i.e. separate pieces of 'land' (connected blocks that will 
-// be rendered) and 'water' (connected holes in between) with integers. Each connected block of land
-// gets a unique odd integer, each connected block of water a unique even integer.
-//
-// Water on the edges of the grid is also considered connected to itself!   
-//   
-// This function also creates a list of 'Edges'. Each Edge is a data structure from which later on we
-// will start building the side walls of each connected block of land (and sides of holes of water
-// inside). Each Edge needs to point from Land to Water (thus the '(SOUTH,i-1,j)' below) - otherwise
-// later on setting up normal vectors wouldn't work.
-   
-  private void markRegions()
-     {
-     int i, j, numWater=1, numLand=0;
-     
-     for(i=0; i<mRows;i++) if( mCubes[      i][      0]==0 ) markRegion((short)2,      i,       0);
-     for(i=0; i<mRows;i++) if( mCubes[      i][mCols-1]==0 ) markRegion((short)2,      i, mCols-1);
-     for(i=0; i<mCols;i++) if( mCubes[0      ][      i]==0 ) markRegion((short)2,      0,       i);
-     for(i=0; i<mCols;i++) if( mCubes[mRows-1][      i]==0 ) markRegion((short)2,mRows-1,       i);
-           
-     for(i=0; i<mRows; i++)
-        for(j=0; j<mCols; j++)
-           {
-           if( mCubes[i][j] == 0 ) { numWater++; markRegion( (short)(2*numWater ),i,j); mEdges.add(new Edge(SOUTH,i-1,j)); }
-           if( mCubes[i][j] == 1 ) { numLand ++; markRegion( (short)(2*numLand+1),i,j); mEdges.add(new Edge(NORTH,i  ,j)); }
-           }
-     
-     // now we potentially need to kick out some Edges . Otherwise the following does not work:
-     //
-     // 0 1 0
-     // 1 0 1
-     // 0 1 0
-     
-     mEdgeNum= mEdges.size();
-     int initCol, initRow, initSide, lastSide;
-     Edge e1,e2;
-     
-     for(i=0; i<mEdgeNum; i++)
-       {
-       e1 = mEdges.get(i);
-       initRow= e1.row;
-       initCol= e1.col;
-       initSide=e1.side;
-
-       do
-         {
-         //android.util.Log.d("CUBES", "checking edge "+debug(e1));
-
-         mSideWalls++;
-
-         if( e1.side==NORTH || e1.side==SOUTH )
-           {
-           for(j=i+1;j<mEdgeNum;j++)
-             {
-             e2 = mEdges.get(j);
-
-             if( e2.side==e1.side && e2.row==e1.row && e2.col==e1.col )
-               {
-               mEdges.remove(j);
-               mEdgeNum--;
-               j--;
-
-               //android.util.Log.e("CUBES", "removing edge "+debug(e2));
-               }
-             }
-           }
-
-         lastSide = e1.side;
-         e1 = getNextEdge(e1);
-         if( e1.side!=lastSide ) mSideBends++;
-         }
-       while( e1.col!=initCol || e1.row!=initRow || e1.side!=initSide );
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// when calling, make sure that newVal != val
-   
-  private void markRegion(short newVal, int row, int col)
-     {
-     int val = mCubes[row][col];
-     mCubes[row][col] = newVal;
-     
-     if( row>0       && mCubes[row-1][col  ]==val ) markRegion(newVal, row-1, col  );
-     if( row<mRows-1 && mCubes[row+1][col  ]==val ) markRegion(newVal, row+1, col  );
-     if( col>0       && mCubes[row  ][col-1]==val ) markRegion(newVal, row  , col-1);
-     if( col<mCols-1 && mCubes[row  ][col+1]==val ) markRegion(newVal, row  , col+1);
-     }
-   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  private void createNormals(boolean front, int row, int col)
-     {
-     int td,lr; 
-      
-     int nw = (col>0       && row>0      ) ? (mCubes[row-1][col-1]%2) : 0;
-     int w  = (col>0                     ) ? (mCubes[row  ][col-1]%2) : 0;
-     int n  = (               row>0      ) ? (mCubes[row-1][col  ]%2) : 0;
-     int c  =                                (mCubes[row  ][col  ]%2);
-     int sw = (col>0       && row<mRows-1) ? (mCubes[row+1][col-1]%2) : 0;
-     int s  = (               row<mRows-1) ? (mCubes[row+1][col  ]%2) : 0;
-     int ne = (col<mCols-1 && row>0      ) ? (mCubes[row-1][col+1]%2) : 0;
-     int e  = (col<mCols-1               ) ? (mCubes[row  ][col+1]%2) : 0;
-     int se = (col<mCols-1 && row<mRows-1) ? (mCubes[row+1][col+1]%2) : 0;
-
-     if(front)
-       {
-       mNormalZ[0] = 1.0f;
-       mNormalZ[1] = 1.0f;
-       mNormalZ[2] = 1.0f;
-       mNormalZ[3] = 1.0f;
-       }
-     else
-       {
-       mNormalZ[0] =-1.0f;
-       mNormalZ[1] =-1.0f;
-       mNormalZ[2] =-1.0f;
-       mNormalZ[3] =-1.0f;
-       }
-
-     td = nw+n-w-c;
-     lr = c+n-w-nw;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[0] = lr*R;
-     mNormalY[0] = td*R;
-     
-     td = w+c-sw-s;
-     lr = c+s-w-sw;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[1] = lr*R;
-     mNormalY[1] = td*R;
-     
-     td = n+ne-c-e;
-     lr = e+ne-c-n;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[2] = lr*R;
-     mNormalY[2] = td*R;
-     
-     td = c+e-s-se;
-     lr = e+se-c-s;
-     if( td<0 ) td=-1;
-     if( td>0 ) td= 1;
-     if( lr<0 ) lr=-1;
-     if( lr>0 ) lr= 1;
-     mNormalX[3] = lr*R;
-     mNormalY[3] = td*R;
-     /*
-     android.util.Log.d("CUBES", "row="+row+" col="+col);
-     android.util.Log.d("CUBES", mNormalX[0]+" "+mNormalY[0]);
-     android.util.Log.d("CUBES", mNormalX[1]+" "+mNormalY[1]);
-     android.util.Log.d("CUBES", mNormalX[2]+" "+mNormalY[2]);
-     android.util.Log.d("CUBES", mNormalX[3]+" "+mNormalY[3]);
-     */
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int buildFrontBackGrid(boolean front, int vertex, float[] attribs)
-     {
-     int last, current;
-     boolean seenLand=false;
-     boolean lastBlockIsNE = false;
-     boolean currentBlockIsNE;
-     float vectZ = (front ? 0.5f : -0.5f);
-
-     //android.util.Log.d("CUBES", "buildFrontBack");
-
-     for(int row=0; row<mRows; row++)
-       {
-       last =0;
-         
-       for(int col=0; col<mCols; col++)
-         {
-         current = mCubes[row][col];
-
-         if( current%2 == 1 )
-           {
-           currentBlockIsNE = isNE(row,col);
-
-           if( !seenLand && !front && ((vertex%2==1)^currentBlockIsNE) )
-             {
-             //android.util.Log.d("CUBES","repeating winding2 vertex");
-
-             vertex = repeatLast(vertex,attribs);
-             }
-
-           createNormals(front,row,col);
-
-           if( currentBlockIsNE )
-             {
-             if( (last!=current) || !lastBlockIsNE )
-               {
-               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
-               vertex= addFrontVertex( vertex, 0, vectZ, col, row, attribs);
-               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
-               if( !lastBlockIsNE || (!front && !seenLand) ) vertex = repeatLast(vertex,attribs);
-               vertex= addFrontVertex( vertex, 1, vectZ, col, row+1, attribs);
-               }
-             vertex= addFrontVertex( vertex, 2, vectZ, col+1, row, attribs);
-             vertex= addFrontVertex( vertex, 3, vectZ, col+1, row+1, attribs);
-             }
-           else
-             {
-             if( (last!=current) || lastBlockIsNE )
-               {
-               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
-               vertex= addFrontVertex( vertex, 1, vectZ, col, row+1, attribs);
-               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
-               if( lastBlockIsNE || (!front && !seenLand) ) vertex = repeatLast(vertex,attribs);
-               vertex= addFrontVertex( vertex, 0, vectZ, col, row, attribs);
-               }
-             vertex= addFrontVertex( vertex, 3, vectZ, col+1, row+1, attribs);
-             vertex= addFrontVertex( vertex, 2, vectZ, col+1, row  , attribs);
-             }
-
-           seenLand = true;
-           lastBlockIsNE = currentBlockIsNE;
-           }
-            
-         last = current;
-         }
-       }
-     
-     return vertex;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int repeatLast(int vertex, float[] attribs)
-     {
-     //android.util.Log.e("CUBES", "repeating last vertex!");
-
-     if( vertex>0 )
-       {
-       remainingVert--;
-
-       attribs[8*vertex  ] = attribs[8*vertex-8];
-       attribs[8*vertex+1] = attribs[8*vertex-7];
-       attribs[8*vertex+2] = attribs[8*vertex-6];
-       attribs[8*vertex+3] = attribs[8*vertex-5];
-       attribs[8*vertex+4] = attribs[8*vertex-4];
-       attribs[8*vertex+5] = attribs[8*vertex-3];
-       attribs[8*vertex+6] = attribs[8*vertex-2];
-       attribs[8*vertex+7] = attribs[8*vertex-1];
-         
-       vertex++;     
-       }
-     
-     return vertex;
-     }
-   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int buildSideGrid(int vertex, float[] attribs)
-     {
-     //android.util.Log.d("CUBES", "buildSide");
-
-     for(int i=0; i<mEdgeNum; i++)
-       {
-       vertex = buildIthSide(mEdges.get(i), vertex, attribs);
-       } 
-      
-     return vertex;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int buildIthSide(Edge curr, int vertex, float[] attribs)
-     {
-     Edge prev, next;
-     int col, row, side;
-
-     if( curr.side==NORTH ) // water outside
-       {
-       prev = new Edge(WEST,curr.row,curr.col);
-       }
-     else                   // land outside; we need to move forward one link because we are
-       {                    // going in opposite direction and we need to start from a bend.
-       prev = curr;
-       curr = new Edge(EAST,curr.row+1,curr.col-1);
-       }
-
-     for(int i=0; i<mSlices; i++)
-       {
-       col = curr.col;
-       row = curr.row;
-       side= curr.side;
-       next = getNextEdge(curr);
-     
-       addSideVertex(curr,true,i+1,prev.side,vertex++,attribs);
-
-       do
-         {
-         if( prev.side!=curr.side )
-           {
-           addSideVertex(curr,true,i+1,prev.side,vertex++,attribs);
-           addSideVertex(curr,true,i  ,prev.side,vertex++,attribs);
-           }
-       
-         addSideVertex(curr,false,i+1,next.side,vertex++,attribs);
-         addSideVertex(curr,false,i  ,next.side,vertex++,attribs);
-       
-         prev = curr;
-         curr = next;
-         next = getNextEdge(curr);
-         }
-       while( curr.col!=col || curr.row!=row || curr.side!=side );
-     
-       vertex = repeatLast(vertex,attribs);
-       }
-
-     return vertex;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private Edge getNextEdge(Edge curr)
-     {
-     int col = curr.col;
-     int row = curr.row;
-      
-     //android.util.Log.e("CUBES", "row="+row+" col="+col+" mRows="+mRows+" mCols="+mCols);
-                       
-     switch(curr.side) 
-       {
-       case NORTH: if( col==mCols-1 ) 
-                     return new Edge(EAST,row,col);
-                   if( row>0 && mCubes[row-1][col+1]==mCubes[row][col] )
-                     return new Edge(WEST,row-1,col+1);
-                   if( mCubes[row][col+1]==mCubes[row][col] )
-                     return new Edge(NORTH,row,col+1);
-                   else  
-                     return new Edge(EAST,row,col);
-                   
-       case SOUTH: if( col==0 ) 
-                     return new Edge(WEST,row,col);
-                   if( (row<mRows-1) && mCubes[row+1][col-1]==mCubes[row][col] )
-                     return new Edge(EAST,row+1,col-1); 
-                   if( mCubes[row][col-1]==mCubes[row][col] )
-                     return new Edge(SOUTH,row,col-1);
-                   else
-                     return new Edge(WEST,row,col); 
-                     
-       case EAST : if( row==mRows-1 ) 
-                     return new Edge(SOUTH,row,col);
-                   if( (col<mCols-1) && mCubes[row+1][col+1]==mCubes[row][col] )
-                     return new Edge(NORTH,row+1,col+1);
-                   if( mCubes[row+1][col]==mCubes[row][col] )
-                     return new Edge(EAST,row+1,col);
-                   else 
-                     return new Edge(SOUTH,row,col);
-                   
-       default   : if( row==0 )
-                     return new Edge(NORTH,row,col);
-                   if( col>0 && mCubes[row-1][col-1]==mCubes[row][col] )
-                     return new Edge(SOUTH,row-1,col-1);
-                   if( mCubes[row-1][col]==mCubes[row][col] )
-                     return new Edge(WEST,row-1,col);
-                   else
-                     return new Edge(NORTH,row,col);     
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int addFrontVertex(int vertex, int index, float vectZ, int col, int row, float[] attribs)
-     {
-     remainingVert--;
-
-     float x = (float)col/mCols;
-     float y = (float)row/mRows;
-
-     attribs[8*vertex  ] = x-0.5f;
-     attribs[8*vertex+1] = 0.5f-y;
-     attribs[8*vertex+2] = vectZ;
-     attribs[8*vertex+3] = mNormalX[index];
-     attribs[8*vertex+4] = mNormalY[index];
-     attribs[8*vertex+5] = mNormalZ[index];
-     attribs[8*vertex+6] = x;
-     attribs[8*vertex+7] = 1.0f-y;
-
-     return vertex+1;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  private void addSideVertex(Edge curr, boolean back, int slice,int side, int vertex, float[] attribs)
-     {
-     //android.util.Log.e("CUBES", "adding Side vertex!");
-
-     remainingVert--;
-
-     float x, y;
-
-     switch(curr.side)
-       {
-       case NORTH: x = (float)(back ? (curr.col  ):(curr.col+1))/mCols;
-
-                   attribs[8*vertex  ] = x - 0.5f;
-                   attribs[8*vertex+1] = 0.5f - (float)curr.row/mRows;
-                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
-                   attribs[8*vertex+3] = side==NORTH ? 0.0f : (side==WEST?-R:R);
-                   attribs[8*vertex+4] = 1.0f;
-                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
-                   attribs[8*vertex+6] = x;
-                   attribs[8*vertex+7] = 1.0f-(float)(curr.row-slice)/mRows;
-                   break;
-       case SOUTH: x = (float)(back ? (curr.col+1):(curr.col  ))/mCols;
-
-                   attribs[8*vertex  ] = x - 0.5f;
-                   attribs[8*vertex+1] = 0.5f - (float)(curr.row+1)/mRows;
-                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
-                   attribs[8*vertex+3] = side==SOUTH ? 0.0f: (side==EAST?-R:R);
-                   attribs[8*vertex+4] =-1.0f;
-                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
-                   attribs[8*vertex+6] = x;
-                   attribs[8*vertex+7] = 1.0f - (float)(curr.row+1+slice)/mRows;
-                   break;
-       case WEST : y = (float)(back  ? (curr.row+1):(curr.row))/mRows;
-
-                   attribs[8*vertex  ] = (float)curr.col/mCols -0.5f;
-                   attribs[8*vertex+1] = 0.5f - y;
-                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
-                   attribs[8*vertex+3] =-1.0f;
-                   attribs[8*vertex+4] = side==WEST ? 0.0f : (side==NORTH?-R:R);
-                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
-                   attribs[8*vertex+6] = (float)(curr.col-slice)/mCols;
-                   attribs[8*vertex+7] = 1.0f - y;
-                   break;
-       case EAST : y = (float)(back  ? (curr.row):(curr.row+1))/mRows;
-
-                   attribs[8*vertex  ] = (float)(curr.col+1)/mCols -0.5f;
-                   attribs[8*vertex+1] = 0.5f - y;
-                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
-                   attribs[8*vertex+3] = 1.0f;
-                   attribs[8*vertex+4] = side==EAST ? 0.0f : (side==SOUTH?-R:R);
-                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
-                   attribs[8*vertex+6] = (float)(curr.col+1+slice)/mCols;
-                   attribs[8*vertex+7] = 1.0f - y;
-                   break;
-       }
-     
-     if(attribs[8*vertex+6]>1.0f) attribs[8*vertex+6] = 2.0f-attribs[8*vertex+6];
-     if(attribs[8*vertex+6]<0.0f) attribs[8*vertex+6] =     -attribs[8*vertex+6];
-     if(attribs[8*vertex+7]>1.0f) attribs[8*vertex+7] = 2.0f-attribs[8*vertex+7];
-     if(attribs[8*vertex+7]<0.0f) attribs[8*vertex+7] =     -attribs[8*vertex+7];
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void build()
-     {
-     int vertSoFar=0;
-     float[] attribs= new float[(POS_DATA_SIZE+NOR_DATA_SIZE+TEX_DATA_SIZE)*numVertices];
-
-     //android.util.Log.d("MeshCubes","building front grid...");
-
-     vertSoFar = buildFrontBackGrid(true, vertSoFar,attribs);
-
-     if( mSlices>0 )
-       {
-       vertSoFar = repeatLast(vertSoFar,attribs);
-       if( vertSoFar%2==1 )
-         {
-         //android.util.Log.d("MeshCubes","repeating winding1 vertex");
-
-         vertSoFar = repeatLast(vertSoFar,attribs);
-         }
-
-       //android.util.Log.d("MeshCubes","building side grid...");
-
-       vertSoFar = buildSideGrid (vertSoFar,attribs);
-
-       //android.util.Log.d("MeshCubes","building back grid...");
-
-       buildFrontBackGrid (false,vertSoFar,attribs);
-       }
-
-     //android.util.Log.e("MeshCubes", "dataLen="+numVertices);
-     //android.util.Log.d("MeshCubes", "attribs: "+debug(attribs,8) );
-
-     mEdges.clear();
-     mEdges = null;
-     mCubes = null;
-
-     if( remainingVert!=0 )
-       android.util.Log.e("MeshCubes", "remainingVert " +remainingVert );
-
-     mVertAttribs = ByteBuffer.allocateDirect(numVertices*VERTSIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
-     mVertAttribs.put(attribs).position(0);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Creates the underlying mesh of vertices, normals, texture coords.
- *    
- * @param cols   Integer helping to parse the next parameter.
- * @param desc   String describing the subset of a MxNx1 cuboid that we want to create.
- *               Its MxN characters - all 0 or 1 - decide of appropriate field is taken or not.
- *               <p></p>
- *               <p>
- *               <pre>
- *               For example, (cols=2, desc="111010") describes the following shape:
- *
- *               XX
- *               X
- *               X
- *
- *               whereas (cols=2,desc="110001") describes
- *
- *               XX
- *
- *                X
- *               </pre>
- *               </p>
- * @param slices Number of slices, i.e. 'depth' of the Mesh.
- */
- public MeshCubes(int cols, String desc, int slices)
-   {
-   super( (float)slices/cols);
-   prepareDataStructures(cols,desc,slices);
-   build();
-   }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Creates a full, hole-less underlying mesh of vertices, normals, texture coords and colors.
- *
- * @param cols   Number of columns, i.e. 'width' of the Mesh.
- * @param rows   Number of rows, i.e. 'height' of the Mesh.
- * @param slices Number of slices, i.e. 'depth' of the Mesh.
- */
- public MeshCubes(int cols, int rows, int slices)
-   {
-   super( (float)slices/cols);
-   prepareDataStructures(cols,rows,slices);
-   build();
-   }
- }
diff --git a/src/main/java/org/distorted/library/MeshFlat.java b/src/main/java/org/distorted/library/MeshFlat.java
deleted file mode 100644
index 1693302..0000000
--- a/src/main/java/org/distorted/library/MeshFlat.java
+++ /dev/null
@@ -1,193 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Create a flat, rectangular grid.
- * <p>
- * Perfect if you just want to display a flat Texture. If you are not planning to apply any VERTEX
- * effects to it, use MeshFlat(1,1), i.e. a Quad. Otherwise, create more vertices for more realistic effects!
- */
-public class MeshFlat extends MeshObject
-  {
-  private int mCols, mRows;
-  private int remainingVert;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Create a flat, full grid.
-
-  private void computeNumberOfVertices(int cols, int rows)
-     {
-     mRows=rows;
-     mCols=cols;
-
-     if( cols==1 && rows==1 )
-       {
-       numVertices = 4;
-       }
-     else
-       {
-       numVertices = 2*( mRows*mCols +2*mRows - 1) +2*(mCols>=2 ? mRows:0) +
-                     (mCols>=2 && mRows>=2 ? 2*mRows-2 : 1);
-       }
-
-     //android.util.Log.e("BITMAP","vertices="+numVertices+" rows="+mRows+" cols="+mCols);
-
-     remainingVert = numVertices;
-     }
-
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int addVertex(int vertex, float x, float y, float[] attribs)
-     {
-     remainingVert--;
-
-     attribs[8*vertex  ] = x-0.5f;
-     attribs[8*vertex+1] = 0.5f-y;
-     attribs[8*vertex+2] = 0;
-
-     attribs[8*vertex+3] = 0.0f;
-     attribs[8*vertex+4] = 0.0f;
-     attribs[8*vertex+5] = 1.0f;
-
-     attribs[8*vertex+6] = x;
-     attribs[8*vertex+7] = 1.0f-y;
-
-     return vertex+1;
-     }
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int repeatLast(int vertex, float[] attribs)
-     {
-     remainingVert--;
-
-     //android.util.Log.e("BITMAP", "repeating last vertex!");
-
-     if( vertex>0 )
-       {
-       attribs[8*vertex  ] = attribs[8*vertex-8];
-       attribs[8*vertex+1] = attribs[8*vertex-7];
-       attribs[8*vertex+2] = attribs[8*vertex-6];
-       attribs[8*vertex+3] = attribs[8*vertex-5];
-       attribs[8*vertex+4] = attribs[8*vertex-4];
-       attribs[8*vertex+5] = attribs[8*vertex-3];
-       attribs[8*vertex+6] = attribs[8*vertex-2];
-       attribs[8*vertex+7] = attribs[8*vertex-1];
-
-       vertex++;
-       }
-
-     return vertex;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void buildGrid(float[] attribs)
-     {
-     boolean lastBlockIsNE = false;
-     boolean currentBlockIsNE;
-     int vertex = 0;
-
-     float x,y;
-     final float X = 1.0f/mCols;
-     final float Y = 1.0f/mRows;
-
-     //android.util.Log.d("BITMAP", "buildGrid");
-
-     y = 0.0f;
-
-     for(int row=0; row<mRows; row++)
-       {
-       x = 0.0f;
-
-       for(int col=0; col<mCols; col++)
-         {
-         currentBlockIsNE = (2*row<=mRows-1)^(2*col<=mCols-1);
-
-         if( col==0 || (lastBlockIsNE^currentBlockIsNE) )
-           {
-           if( row!=0 && col==0 ) vertex = repeatLast(vertex,attribs);
-           vertex= addVertex( vertex, x, y+(currentBlockIsNE?0:Y), attribs);
-           if( row!=0 && col==0 ) vertex = repeatLast(vertex,attribs);
-           if( lastBlockIsNE^currentBlockIsNE)  vertex = repeatLast(vertex,attribs);
-           vertex= addVertex( vertex, x, y+(currentBlockIsNE?Y:0), attribs);
-           }
-         vertex= addVertex( vertex, x+X, y+(currentBlockIsNE?0:Y), attribs);
-         vertex= addVertex( vertex, x+X, y+(currentBlockIsNE?Y:0), attribs);
-
-         lastBlockIsNE = currentBlockIsNE;
-         x+=X;
-         }
-
-       y+=Y;
-       }
-
-     //android.util.Log.d("BITMAP", "buildGrid done");
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/*
-  private static String debug(float[] val, int stop)
-     {
-     String ret="";
-
-     for(int i=0; i<val.length; i++)
-        {
-        if( i%stop==0 ) ret+="\n";
-        ret+=(" "+val[i]);
-        }
-
-     return ret;
-     }
-*/
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Creates the underlying grid of vertices, normals and texture coords.
- *
- * @param cols Number of columns in the grid.
- * @param rows Number of rows in the grid.
- */
- public MeshFlat(int cols, int rows)
-    {
-    super(0.0f);
-    computeNumberOfVertices(cols,rows);
-
-    float[] attribs= new float[(POS_DATA_SIZE+NOR_DATA_SIZE+TEX_DATA_SIZE)*numVertices];
-
-    buildGrid(attribs);
-
-    //android.util.Log.e("MeshFlat", "dataLen="+numVertices);
-    //android.util.Log.d("MeshFlat", "attribs: "+debug(attribs,8) );
-
-    if( remainingVert!=0 )
-      android.util.Log.d("BITMAP", "remainingVert " +remainingVert );
-
-    mVertAttribs = ByteBuffer.allocateDirect(numVertices*VERTSIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
-    mVertAttribs.put(attribs).position(0);
-    }
- }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/library/MeshObject.java b/src/main/java/org/distorted/library/MeshObject.java
deleted file mode 100644
index aa90332..0000000
--- a/src/main/java/org/distorted/library/MeshObject.java
+++ /dev/null
@@ -1,142 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library;
-
-import android.opengl.GLES30;
-
-import java.nio.FloatBuffer;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * Abstract class which represents a Mesh, ie 3 arrays of Vertex attributes: 1) positions
- * 2) normals 3) texture coordinates.
- * <p>
- * If you want to render to a particular shape, extend from here, construct the attrib FloatBuffer
- * and provide correct numVertices.
- */
-public abstract class MeshObject extends DistortedObject
-   {
-   private static final int BYTES_PER_FLOAT = 4;
-
-   static final int POS_DATA_SIZE= 3;
-   static final int NOR_DATA_SIZE= 3;
-   static final int TEX_DATA_SIZE= 2;
-
-   static final int OFFSET0 =                                                           0;
-   static final int OFFSET1 = (POS_DATA_SIZE                            )*BYTES_PER_FLOAT;
-   static final int OFFSET2 = (POS_DATA_SIZE+NOR_DATA_SIZE              )*BYTES_PER_FLOAT;
-
-   static final int TFSIZE  = (POS_DATA_SIZE+POS_DATA_SIZE              )*BYTES_PER_FLOAT;
-   static final int VERTSIZE= (POS_DATA_SIZE+NOR_DATA_SIZE+TEX_DATA_SIZE)*BYTES_PER_FLOAT;
-
-   boolean mShowNormals;
-
-   int numVertices;
-   FloatBuffer mVertAttribs;   // packed: PosX,PosY,PosZ, NorX, NorY,NorZ, TexS, TexT
-   int[] mAttVBO = new int[1]; // server-side packed vertex attributes
-   int[] mAttTFO = new int[1]; // transform feedback
-
-   final float zFactor;        // strange workaround for the fact that we need to somehow store the 'depth'
-                               // of the Mesh. Used in DistortedEffects. See DistortedTexture.getDepth().
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   MeshObject(float factor)
-     {
-     super(DistortedObject.NOT_CREATED_YET,DistortedObject.TYPE_USER);
-
-     zFactor = factor;
-     mShowNormals = false;
-     recreate();
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// must be called from a thread holding OpenGL Context
-//
-// Do NOT release mVertAttribs etc as we will need them when we need to re-create the buffers after
-// a loss of OpenGL context!
-
-   void create()
-     {
-     if( mAttVBO[0]<0 )
-       {
-       GLES30.glGenBuffers(1, mAttVBO, 0);
-       GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mAttVBO[0]);
-       GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, numVertices*VERTSIZE, mVertAttribs, GLES30.GL_STATIC_READ);
-       GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
-       }
-     if( mAttTFO[0]<0 && Distorted.GLSL >= 300 )
-       {
-       GLES30.glGenBuffers(1, mAttTFO, 0);
-       GLES30.glBindBuffer(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, mAttTFO[0]);
-       GLES30.glBufferData(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, numVertices*TFSIZE, null, GLES30.GL_STATIC_READ);
-       GLES30.glBindBuffer(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, 0);
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// must be called from a thread holding OpenGL Context
-
-   void delete()
-     {
-     if( mAttVBO[0]>=0 )
-       {
-       GLES30.glDeleteBuffers(1, mAttVBO, 0);
-       mAttVBO[0] = -1;
-       }
-     if( mAttTFO[0]>=0 )
-       {
-       GLES30.glDeleteBuffers(1, mAttTFO, 0);
-       mAttTFO[0] = -1;
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   void recreate()
-     {
-     mAttVBO[0] = -1;
-     mAttTFO[0] = -1;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// debugging only
-
-   String printDetails()
-     {
-     return getClass().getSimpleName()+" vertices:"+ numVertices;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-/**
- * When rendering this Mesh, do we want to render the Normal vectors as well?
- * <p>
- * Will work only on OpenGL ES >= 3.0 devices.
- *
- * @param show Controls if we render the Normal vectors or not.
- */
-   public void setShowNormals(boolean show)
-     {
-     mShowNormals = (Distorted.GLSL >= 300 && show);
-     }
-   }
-
-
-
diff --git a/src/main/java/org/distorted/library/effect/EffectMessageSender.java b/src/main/java/org/distorted/library/effect/EffectMessageSender.java
deleted file mode 100644
index 111b76c..0000000
--- a/src/main/java/org/distorted/library/effect/EffectMessageSender.java
+++ /dev/null
@@ -1,143 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.effect;
-
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.message.EffectMessage;
-
-import java.util.Vector;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-final class EffectMessageSender extends Thread
-  {
-  private class Message
-    {
-    EffectListener mListener;
-    EffectMessage mMessage;
-    long mEffectID;
-    int mEffectName;
-    long mBitmapID;
-
-    Message(EffectListener l, EffectMessage m, long id, int name, long bmpID)
-      {
-      mListener   = l;
-      mMessage    = m;
-      mEffectID   = id;
-      mEffectName = name;
-      mBitmapID   = bmpID;
-      }
-    }
-  
-  private static Vector<Message> mList =null;
-  private static EffectMessageSender mThis=null;
-  private static volatile boolean mNotify  = false;
-  private static volatile boolean mRunning = false;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  private EffectMessageSender() 
-    {
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void startSending()
-    {
-    mRunning = true;
-
-    if( mThis==null )
-      {
-      mList = new Vector<>();
-      mThis = new EffectMessageSender();
-      mThis.start();
-      }
-    else  
-      {  
-      synchronized(mThis)
-        {
-        mThis.notify();
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  static void stopSending()
-    {
-    mRunning = false;
-
-    if( mThis!=null )
-      {
-      synchronized(mThis)
-        {
-        mThis.notify();
-        }
-      }
-    }
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  public void run()
-    {
-    Message tmp;  
-     
-    while(mRunning)
-      {
-      //android.util.Log.i("SENDER", "sender thread running...");
-
-      while( mList.size()>0 )
-        {
-        tmp = mList.remove(0);
-        tmp.mListener.effectMessage(tmp.mMessage, tmp.mEffectID, tmp.mEffectName,tmp.mBitmapID);
-        }
-
-      synchronized(mThis)
-        {
-        if (!mNotify)
-          {
-          try  { mThis.wait(); }
-          catch(InterruptedException ex) { }
-          }
-        mNotify = false;
-        }
-      }
-
-    mThis = null;
-    mList.clear();
-
-    //android.util.Log.i("SENDER", "sender thread finished...");
-    }
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-        
-  static void newMessage(EffectListener l, EffectMessage m, long id, int name, long bmpID)
-    {
-    Message msg = mThis.new Message(l,m,id,name,bmpID);
-    mList.add(msg);
-
-    synchronized(mThis)
-      {
-      mNotify = true;
-      mThis.notify();
-      }
-    }
-  }
-///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/effect/EffectQueue.java b/src/main/java/org/distorted/library/effect/EffectQueue.java
deleted file mode 100644
index 6bc1d38..0000000
--- a/src/main/java/org/distorted/library/effect/EffectQueue.java
+++ /dev/null
@@ -1,366 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.effect;
-
-import org.distorted.library.Distorted;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.type.Dynamic;
-
-import java.util.Vector;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class EffectQueue
-  {
-  protected byte mNumEffects;   // number of effects at the moment
-  protected long mTotalEffects; // total number of effects ever created
-  protected int[] mName;
-  protected int[] mType;
-  protected float[] mUniforms;
-  protected float[] mCache;
-  protected Dynamic[][] mInter;
-  protected long[] mCurrentDuration;
-  protected byte[] mFreeIndexes;
-  protected byte[] mIDIndex;
-  protected long[] mID;
-  protected long mTime=0;
-  protected static int[] mMax = new int[Effect.LENGTH];
-  protected int mMaxIndex;
-  protected Vector<EffectListener> mListeners =null;
-  protected int mNumListeners=0;  // ==mListeners.length(), but we only create mListeners if the first one gets added
-  protected long mObjectID;
-
-  private static boolean mCreated;
-
-  static
-    {
-    onDestroy();
-    }
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueue(long id, int numUniforms, int numCache, int index)
-    {
-    mNumEffects   = 0;
-    mTotalEffects = 0;
-    mMaxIndex     = index;
-    mObjectID     = id;
-
-    int max = mMax[mMaxIndex];
-
-    if( max>0 )
-      {
-      mName            = new int[max];
-      mType            = new int[max];
-      mUniforms        = new float[numUniforms*max];
-      mInter           = new Dynamic[3][max];
-      mCurrentDuration = new long[max];
-      mID              = new long[max];
-      mIDIndex         = new byte[max];
-      mFreeIndexes     = new byte[max];
-     
-      for(byte i=0; i<max; i++) mFreeIndexes[i] = i;
-
-      if( numCache>0 )
-        {
-        mCache = new float[numCache*max];
-        }
-      }
-   
-    mCreated = true;  
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  @SuppressWarnings("unused")
-  int getNumEffects()
-    {
-    return mNumEffects;  
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Only max Byte.MAX_VALUE concurrent effects per DistortedEffects object.
-// If you want more, change type of the mNumEffects, mIDIndex and mFreeIndexes variables to shorts.
-// (although probably this many uniforms will not fit in the shaders anyway!)
-
-  static boolean setMax(int index, int m)
-    {
-    if( (!mCreated && !Distorted.isInitialized()) || m<=mMax[index] )
-      {
-      if( m<0              ) m = 0;
-      else if( m>Byte.MAX_VALUE ) m = Byte.MAX_VALUE;
-
-      mMax[index] = m;
-      return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static int getMax(int index)
-    {
-    return mMax[index];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void registerForMessages(EffectListener el)
-    {
-    if( mListeners==null ) mListeners = new Vector<>(2,2);
-
-    if( !mListeners.contains(el) )
-      {
-      mListeners.add(el);
-      mNumListeners++;
-      }
-    }
- 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void deregisterForMessages(EffectListener el)
-    {
-    if( mListeners.remove(el) )
-      {
-      mNumListeners--;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void onDestroy()
-    {
-    Effect.reset(mMax);
-    mCreated = false;  
-    }
- 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized int removeByID(long id)
-    {
-    int i = getEffectIndex(id);
-   
-    if( i>=0 ) 
-      {
-      remove(i);
-      return 1;
-      }
-   
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized int removeByType(Effect effect)
-    {
-    int ret  = 0;
-    int name = effect.getName();
-    int type = effect.getType();
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      if( mName[i]==name && mType[i]==type )
-        {
-        remove(i);
-        i--;
-        ret++;
-        }
-      }
-   
-    return ret;
-    }
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  private synchronized int getEffectIndex(long id)
-    {
-    int index = mIDIndex[(int)(id%mMax[mMaxIndex])];
-    return (index<mNumEffects && mID[index]==id ? index : -1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// we do want to notify Listeners if they called 'abortAll' themselves but don't want to notify
-// them if it is the library itself which is releasing resources.
-
-  synchronized int abortAll(boolean notify)
-    {
-    int ret = mNumEffects;
-    long removedID;
-    int removedName, removedType;
-
-    for(int i=0; i<ret; i++ )
-      {
-      mInter[0][i] = null;
-      mInter[1][i] = null;
-      mInter[2][i] = null;
-
-      if( notify )
-        {
-        removedID = mID[i];
-        removedName= mName[i];
-        removedType= mType[i];
-
-        for(int j=0; j<mNumListeners; j++)
-          EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                          EffectMessage.EFFECT_REMOVED,
-                                          (removedID<<Effect.LENGTH)+removedType,
-                                          removedName,
-                                          mObjectID);
-        }
-      }
-
-    mNumEffects= 0;
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// this assumes 0<=effect<mNumEffects
-  
-  protected void remove(int effect)
-    {
-    mNumEffects--;     
-    
-    byte removedIndex = (byte)(mID[effect]%mMax[mMaxIndex]);
-    byte removedPosition = mIDIndex[removedIndex];
-    mFreeIndexes[mNumEffects] = removedIndex;
-    
-    long removedID = mID[effect];
-    int removedName= mName[effect];
-    int removedType= mType[effect];
-
-    for(int j=0; j<mMax[mMaxIndex]; j++)
-      {
-      if( mIDIndex[j] > removedPosition ) mIDIndex[j]--; 
-      }
-         
-    for(int j=effect; j<mNumEffects; j++ ) 
-      {
-      mName[j]            = mName[j+1];
-      mType[j]            = mType[j+1];
-      mInter[0][j]        = mInter[0][j+1];
-      mInter[1][j]        = mInter[1][j+1];
-      mInter[2][j]        = mInter[2][j+1];
-      mCurrentDuration[j] = mCurrentDuration[j+1];
-      mID[j]              = mID[j+1];
-    
-      moveEffect(j);
-      }
-   
-    mInter[0][mNumEffects] = null;
-    mInter[1][mNumEffects] = null;
-    mInter[2][mNumEffects] = null;
-
-    for(int i=0; i<mNumListeners; i++) 
-      EffectMessageSender.newMessage( mListeners.elementAt(i),
-                                      EffectMessage.EFFECT_REMOVED,
-                                      (removedID<<Effect.LENGTH)+removedType,
-                                      removedName,
-                                      mObjectID);
-    }
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  protected long addBase(Effect effect)
-    {
-    int type = effect.getType();
-
-    mName[mNumEffects] = effect.getName();
-    mType[mNumEffects] = type;
-    mCurrentDuration[mNumEffects] = 0;
-    
-    int index = mFreeIndexes[mNumEffects];
-    long id = mTotalEffects*mMax[mMaxIndex] + index;
-    mID[mNumEffects] = id;
-    mIDIndex[index] = mNumEffects;  
-   
-    mNumEffects++; 
-    mTotalEffects++;
-   
-    return (id<<Effect.LENGTH)+type;
-    }
-    
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// used only for debugging
-
-  @SuppressWarnings("unused")
-  protected String printEffects(int max)
-    {
-    long[] indexes = new long[mMax[mMaxIndex]];
-   
-    for(int g=0; g<mMax[mMaxIndex]; g++)
-      {
-      indexes[g] = -1;  
-      }
-   
-    String ret="(";
-    int f;
-   
-    for(int g=0; g<max; g++) 
-      {
-      f = getEffectIndex(g);
-      if( f>=0 ) indexes[f] = g;
-      }
-   
-    for(int g=0; g<mMax[mMaxIndex]; g++)
-      {
-      ret += (g>0 ? ",":"")+(indexes[g]>=0 ? indexes[g] : " ");   
-      }
-   
-    ret += ")";
-   
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Only used for debugging
-  
-  protected boolean printByID(long id)
-    {
-    int index = getEffectIndex(id);
-   
-    if( index>=0 ) 
-      {
-      boolean inter0 = mInter[0][index]==null;
-      boolean inter1 = mInter[1][index]==null;
-      boolean inter2 = mInter[2][index]==null;
-
-      android.util.Log.e("EffectQueue", "numEffects="+mNumEffects+" effect id="+id+" index="+index+
-                         " duration="+mCurrentDuration[index]+" inter[0] null="+inter0+" inter[1] null="+inter1+" inter[2] null="+inter2);
-
-      if( !inter0 ) android.util.Log.e("EffectQueue","inter[0]: "+mInter[0][index].print());
-      if( !inter1 ) android.util.Log.e("EffectQueue","inter[1]: "+mInter[1][index].print());
-      if( !inter2 ) android.util.Log.e("EffectQueue","inter[2]: "+mInter[2][index].print());
-
-      return true;
-      }
-   
-    android.util.Log.e("EffectQueue", "effect id="+id+" not found");
-
-    return false;  
-    }
- 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  abstract void moveEffect(int index);
-  }
-///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/effect/EffectQueueFragment.java b/src/main/java/org/distorted/library/effect/EffectQueueFragment.java
deleted file mode 100644
index 5114bcb..0000000
--- a/src/main/java/org/distorted/library/effect/EffectQueueFragment.java
+++ /dev/null
@@ -1,211 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.effect;
-
-import android.opengl.GLES30;
-
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.type.Dynamic4D;
-import org.distorted.library.type.Static;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.library.type.Static5D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueueFragment extends EffectQueue
-  {
-  private static final int NUM_UNIFORMS = 8;
-  private static final int NUM_CACHE    = 4;
-  private static final int INDEX = Effect.FRAGMENT;
-  private static int mNumEffectsH;
-  private static int mTypeH;
-  private static int mUniformsH;
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueueFragment(long id)
-    { 
-    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void getUniforms(int mProgramH)
-    {
-    mNumEffectsH= GLES30.glGetUniformLocation( mProgramH, "fNumEffects");
-    mTypeH      = GLES30.glGetUniformLocation( mProgramH, "fType");
-    mUniformsH  = GLES30.glGetUniformLocation( mProgramH, "fUniforms");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  synchronized void compute(long currTime) 
-    { 
-    if( currTime==mTime ) return;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-   
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-        {
-        for(int j=0; j<mNumListeners; j++)   
-          EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                          EffectMessage.EFFECT_FINISHED,
-                                          (mID[i]<<Effect.LENGTH)+Effect.FRAGMENT,
-                                          mName[i],
-                                          mObjectID);
-      
-        if( FragmentEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
-          {
-          remove(i);
-          i--;
-          continue;
-          }
-        else mInter[0][i] = null;
-        }
-
-      if( mInter[1][i]!=null ) mInter[2][i].interpolateMain( mUniforms, NUM_UNIFORMS*i+1, mCurrentDuration[i], step);
-      if( mInter[2][i]!=null ) mInter[1][i].interpolateMain( mCache   , NUM_CACHE*i     , mCurrentDuration[i], step);
-      }
-   
-    mTime = currTime;  
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  protected void moveEffect(int index)
-    {
-    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
-    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
-    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
-    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
-
-    mCache[NUM_CACHE*index  ] = mCache[NUM_CACHE*(index+1)  ];
-    mCache[NUM_CACHE*index+1] = mCache[NUM_CACHE*(index+1)+1];
-    mCache[NUM_CACHE*index+2] = mCache[NUM_CACHE*(index+1)+2];
-    mCache[NUM_CACHE*index+3] = mCache[NUM_CACHE*(index+1)+3];
-    }
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  synchronized void send(float halfX, float halfY)
-    {
-    GLES30.glUniform1i( mNumEffectsH, mNumEffects);
-
-    if( mNumEffects>0 )
-      {
-      for(int i=0; i<mNumEffects; i++)
-        {
-        mUniforms[NUM_UNIFORMS*i+4] = mCache[NUM_CACHE*i  ]-halfX;
-        mUniforms[NUM_UNIFORMS*i+5] =-mCache[NUM_CACHE*i+1]+halfY;
-        mUniforms[NUM_UNIFORMS*i+6] = mCache[NUM_CACHE*i+2];
-        mUniforms[NUM_UNIFORMS*i+7] = mCache[NUM_CACHE*i+3];
-        }
-
-      GLES30.glUniform1iv( mTypeH    ,                 mNumEffects, mName    ,0);
-      GLES30.glUniform4fv( mUniformsH,(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
-      }  
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized long add(FragmentEffect fe)
-    {
-    if( mMax[INDEX]>mNumEffects )
-      {
-      int dim0 = 0;
-
-      if( fe.mDynamic0 != null )
-        {
-        mInter[0][mNumEffects] = fe.mDynamic0;
-        dim0 = fe.mDynamic0.getDimension();
-        }
-      else
-        {
-        mInter[0][mNumEffects] = null;
-
-        if( fe.mStatic0 != null )
-          {
-          Static s = fe.mStatic0;
-          dim0 = s.getDimension();
-
-          switch( dim0 )
-            {
-            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
-            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
-            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
-            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
-            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
-            }
-          }
-        }
-
-      if( fe.mDynamic1 != null )
-        {
-        mInter[1][mNumEffects] = fe.mDynamic1;
-        }
-      else
-        {
-        mInter[1][mNumEffects] = null;
-
-        if( fe.mStatic1 != null )
-          {
-          Static s = fe.mStatic1;
-          int dim1 = s.getDimension();
-
-          switch( dim1 )
-            {
-            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 4] = ((Static5D)s).getV();
-            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 3] = ((Static4D)s).getW();
-            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 2] = ((Static3D)s).getZ();
-            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 1] = ((Static2D)s).getY();
-            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0    ] = ((Static1D)s).getX();
-            }
-          }
-        }
-
-      if( fe.mRegion instanceof Dynamic4D)
-        {
-        mInter[2][mNumEffects] = (Dynamic4D)fe.mRegion;
-        }
-      else if( fe.mRegion instanceof Static4D )
-        {
-        mInter[2][mNumEffects]  = null;
-
-        Static4D s = (Static4D)fe.mRegion;
-        mCache[NUM_CACHE*mNumEffects  ] = s.getX();
-        mCache[NUM_CACHE*mNumEffects+1] = s.getY();
-        mCache[NUM_CACHE*mNumEffects+2] = s.getZ();
-        mCache[NUM_CACHE*mNumEffects+3] = s.getW();
-        }
-      else return -1;
-
-      return addBase(fe);
-      }
-      
-    return -1;
-    }
-  }
diff --git a/src/main/java/org/distorted/library/effect/EffectQueueMatrix.java b/src/main/java/org/distorted/library/effect/EffectQueueMatrix.java
deleted file mode 100644
index fb43315..0000000
--- a/src/main/java/org/distorted/library/effect/EffectQueueMatrix.java
+++ /dev/null
@@ -1,441 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.effect;
-
-import android.opengl.GLES30;
-import android.opengl.Matrix;
-
-import org.distorted.library.DistortedOutputSurface;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.type.Dynamic3D;
-import org.distorted.library.type.Static;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.library.type.Static5D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueueMatrix extends EffectQueue
-  {   
-  private static final int NUM_UNIFORMS = 7;
-  private static final int NUM_CACHE    = 0;
-  private static final int INDEX = Effect.MATRIX;
-
-  private static float[] mMVPMatrix = new float[16];
-  private static float[] mTmpMatrix = new float[16];
-  private static float[] mViewMatrix= new float[16];
-
-  private static int mObjDH;      // This is a handle to half a Object dimensions
-  private static int mMVPMatrixH; // the transformation matrix
-  private static int mMVMatrixH;  // the modelview matrix.
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueueMatrix(long id)
-    { 
-    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void multiplyByQuat(float[] matrix, float X, float Y, float Z, float W)
-    {
-    float xx= X * X;
-    float xy= X * Y;
-    float xz= X * Z;
-    float xw= X * W;
-    float yy= Y * Y;
-    float yz= Y * Z;
-    float yw= Y * W;
-    float zz= Z * Z;
-    float zw= Z * W;
-
-    mTmpMatrix[0]  = 1 - 2 * ( yy + zz );
-    mTmpMatrix[1]  =     2 * ( xy - zw );
-    mTmpMatrix[2]  =     2 * ( xz + yw );
-    mTmpMatrix[4]  =     2 * ( xy + zw );
-    mTmpMatrix[5]  = 1 - 2 * ( xx + zz );
-    mTmpMatrix[6]  =     2 * ( yz - xw );
-    mTmpMatrix[8]  =     2 * ( xz - yw );
-    mTmpMatrix[9]  =     2 * ( yz + xw );
-    mTmpMatrix[10] = 1 - 2 * ( xx + yy );
-    mTmpMatrix[3]  = mTmpMatrix[7] = mTmpMatrix[11] = mTmpMatrix[12] = mTmpMatrix[13] = mTmpMatrix[14] = 0;
-    mTmpMatrix[15] = 1;
-    
-    Matrix.multiplyMM(mMVPMatrix, 0, matrix, 0, mTmpMatrix, 0);  
-
-    matrix[ 0] = mMVPMatrix[ 0];
-    matrix[ 1] = mMVPMatrix[ 1];
-    matrix[ 2] = mMVPMatrix[ 2];
-    matrix[ 3] = mMVPMatrix[ 3];
-    matrix[ 4] = mMVPMatrix[ 4];
-    matrix[ 5] = mMVPMatrix[ 5];
-    matrix[ 6] = mMVPMatrix[ 6];
-    matrix[ 7] = mMVPMatrix[ 7];
-    matrix[ 8] = mMVPMatrix[ 8];
-    matrix[ 9] = mMVPMatrix[ 9];
-    matrix[10] = mMVPMatrix[10];
-    matrix[11] = mMVPMatrix[11];
-    matrix[12] = mMVPMatrix[12];
-    matrix[13] = mMVPMatrix[13];
-    matrix[14] = mMVPMatrix[14];
-    matrix[15] = mMVPMatrix[15];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void magnify(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, float marginInPixels)
-    {
-    float scale, nx, ny;
-    float[] result= new float[4];
-    float[] point = new float[4];
-    float[] matrix= new float[16];
-    float minx = Integer.MAX_VALUE;
-    float maxx = Integer.MIN_VALUE;
-    float miny = Integer.MAX_VALUE;
-    float maxy = Integer.MIN_VALUE;
-
-    point[3] = 1.0f;
-
-    Matrix.multiplyMM(matrix, 0, projection.mProjectionMatrix, 0, mViewMatrix, 0);
-
-    point[0] = +halfX; point[1] = +halfY; point[2] = +halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    point[0] = +halfX; point[1] = +halfY; point[2] = -halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    point[0] = +halfX; point[1] = -halfY; point[2] = +halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    point[0] = +halfX; point[1] = -halfY; point[2] = -halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    point[0] = -halfX; point[1] = +halfY; point[2] = +halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    point[0] = -halfX; point[1] = +halfY; point[2] = -halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    point[0] = -halfX; point[1] = -halfY; point[2] = +halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    point[0] = -halfX; point[1] = -halfY; point[2] = -halfZ;
-    Matrix.multiplyMV(result,0,matrix,0,point,0);
-    nx = result[0]/result[3];
-    ny = result[1]/result[3];
-    if( nx<minx ) minx = nx;
-    if( nx>maxx ) maxx = nx;
-    if( ny<miny ) miny = ny;
-    if( ny>maxy ) maxy = ny;
-
-    float xlen = projection.mWidth *(maxx-minx)/2;
-    float ylen = projection.mHeight*(maxy-miny)/2;
-
-    scale = 1.0f + marginInPixels/( xlen>ylen ? ylen:xlen );
-
-    //android.util.Log.d("scale", ""+marginInPixels+" scale= "+scale+" xlen="+xlen+" ylen="+ylen);
-
-    Matrix.scaleM(mViewMatrix, 0, scale, scale, scale);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// here construct the ModelView and the ModelViewProjection Matrices
-
-  private void constructMatrices(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, float marginInPixels)
-    {
-    Matrix.setIdentityM(mViewMatrix, 0);
-    Matrix.translateM(mViewMatrix, 0, -projection.mWidth/2, projection.mHeight/2, -projection.mDistance);
-
-    float x,y,z, sx,sy,sz;
-    float mipmap = projection.mMipmap;
-
-    if( mipmap!=1 ) Matrix.scaleM(mViewMatrix, 0, mipmap, mipmap, mipmap);
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      if (mName[i] == MatrixEffect.ROTATE )
-        {
-        x = mUniforms[NUM_UNIFORMS*i+4];
-        y = mUniforms[NUM_UNIFORMS*i+5];
-        z = mUniforms[NUM_UNIFORMS*i+6];
-
-        Matrix.translateM(mViewMatrix, 0, x,-y, z);
-        Matrix.rotateM( mViewMatrix, 0, mUniforms[NUM_UNIFORMS*i], mUniforms[NUM_UNIFORMS*i+1], mUniforms[NUM_UNIFORMS*i+2], mUniforms[NUM_UNIFORMS*i+3]);
-        Matrix.translateM(mViewMatrix, 0,-x, y,-z);
-        }
-      else if(mName[i] == MatrixEffect.QUATERNION )
-        {
-        x = mUniforms[NUM_UNIFORMS*i+4];
-        y = mUniforms[NUM_UNIFORMS*i+5];
-        z = mUniforms[NUM_UNIFORMS*i+6];
-
-        Matrix.translateM(mViewMatrix, 0, x,-y, z);
-        multiplyByQuat(mViewMatrix, mUniforms[NUM_UNIFORMS*i], mUniforms[NUM_UNIFORMS*i+1], mUniforms[NUM_UNIFORMS*i+2], mUniforms[NUM_UNIFORMS*i+3]);
-        Matrix.translateM(mViewMatrix, 0,-x, y,-z);
-        }
-      else if(mName[i] == MatrixEffect.MOVE )
-        {
-        sx = mUniforms[NUM_UNIFORMS*i  ];
-        sy = mUniforms[NUM_UNIFORMS*i+1];
-        sz = mUniforms[NUM_UNIFORMS*i+2];
-
-        Matrix.translateM(mViewMatrix, 0, sx,-sy, sz);
-        }
-      else if(mName[i] == MatrixEffect.SCALE )
-        {
-        sx = mUniforms[NUM_UNIFORMS*i  ];
-        sy = mUniforms[NUM_UNIFORMS*i+1];
-        sz = mUniforms[NUM_UNIFORMS*i+2];
-
-        Matrix.scaleM(mViewMatrix, 0, sx, sy, sz);
-        }
-      else if(mName[i] == MatrixEffect.SHEAR )
-        {
-        sx = mUniforms[NUM_UNIFORMS*i  ];
-        sy = mUniforms[NUM_UNIFORMS*i+1];
-        sz = mUniforms[NUM_UNIFORMS*i+2];
-
-        x  = mUniforms[NUM_UNIFORMS*i+4];
-        y  = mUniforms[NUM_UNIFORMS*i+5];
-        z  = mUniforms[NUM_UNIFORMS*i+6];
-
-        Matrix.translateM(mViewMatrix, 0, x,-y, z);
-
-        mViewMatrix[4] += sx*mViewMatrix[0]; // Multiply viewMatrix by 1 x 0 0 , i.e. X-shear.
-        mViewMatrix[5] += sx*mViewMatrix[1]; //                        0 1 0 0
-        mViewMatrix[6] += sx*mViewMatrix[2]; //                        0 0 1 0
-        mViewMatrix[7] += sx*mViewMatrix[3]; //                        0 0 0 1
-
-        mViewMatrix[0] += sy*mViewMatrix[4]; // Multiply viewMatrix by 1 0 0 0 , i.e. Y-shear.
-        mViewMatrix[1] += sy*mViewMatrix[5]; //                        y 1 0 0
-        mViewMatrix[2] += sy*mViewMatrix[6]; //                        0 0 1 0
-        mViewMatrix[3] += sy*mViewMatrix[7]; //                        0 0 0 1
-
-        mViewMatrix[4] += sz*mViewMatrix[8]; // Multiply viewMatrix by 1 0 0 0 , i.e. Z-shear.
-        mViewMatrix[5] += sz*mViewMatrix[9]; //                        0 1 0 0
-        mViewMatrix[6] += sz*mViewMatrix[10];//                        0 z 1 0
-        mViewMatrix[7] += sz*mViewMatrix[11];//                        0 0 0 1
-
-        Matrix.translateM(mViewMatrix, 0,-x, y, -z);
-        }
-      }
-
-    Matrix.translateM(mViewMatrix, 0, halfX,-halfY,-halfZ);
-
-    if( marginInPixels!=0 ) magnify(projection,halfX,halfY,halfZ, marginInPixels);
-
-    Matrix.multiplyMM(mMVPMatrix, 0, projection.mProjectionMatrix, 0, mViewMatrix, 0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void getUniforms(int mProgramH)
-    {
-    mObjDH     = GLES30.glGetUniformLocation(mProgramH, "u_objD");
-    mMVPMatrixH= GLES30.glGetUniformLocation(mProgramH, "u_MVPMatrix");
-    mMVMatrixH = GLES30.glGetUniformLocation(mProgramH, "u_MVMatrix"); 
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized void compute(long currTime) 
-    {
-    if( currTime==mTime ) return;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-   
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-        {
-        for(int j=0; j<mNumListeners; j++)
-          EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                          EffectMessage.EFFECT_FINISHED,
-                                         (mID[i]<<Effect.LENGTH)+Effect.MATRIX,
-                                          mName[i],
-                                          mObjectID);
-
-        if( MatrixEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
-          {
-          remove(i);
-          i--;
-          continue;
-          }
-        else mInter[0][i] = null;
-        }
-
-      if( mInter[2][i]!=null )
-        {
-        mInter[2][i].interpolateMain(mUniforms, NUM_UNIFORMS*i+4, mCurrentDuration[i], step);
-        }
-      }
-     
-    mTime = currTime;  
-    }  
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  protected void moveEffect(int index)
-    {
-    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
-    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
-    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
-    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
-    mUniforms[NUM_UNIFORMS*index+4] = mUniforms[NUM_UNIFORMS*(index+1)+4];
-    mUniforms[NUM_UNIFORMS*index+5] = mUniforms[NUM_UNIFORMS*(index+1)+5];
-    mUniforms[NUM_UNIFORMS*index+6] = mUniforms[NUM_UNIFORMS*(index+1)+6];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  float[] getMVP()
-    {
-    return mMVPMatrix;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized void send(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, float marginInPixels)
-    {
-    constructMatrices(projection,halfX,halfY,halfZ, marginInPixels);
-
-    GLES30.glUniform3f( mObjDH , halfX, halfY, halfZ);
-    GLES30.glUniformMatrix4fv(mMVMatrixH , 1, false, mViewMatrix, 0);
-    GLES30.glUniformMatrix4fv(mMVPMatrixH, 1, false, mMVPMatrix , 0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized long add(MatrixEffect me)
-    {
-    if( mMax[INDEX]>mNumEffects )
-      {
-      int dim0 = 0;
-
-      if( me.mDynamic0 != null )
-        {
-        mInter[0][mNumEffects] = me.mDynamic0;
-        dim0 = me.mDynamic0.getDimension();
-        }
-      else
-        {
-        mInter[0][mNumEffects] = null;
-
-        if( me.mStatic0 != null )
-          {
-          Static s = me.mStatic0;
-          dim0 = s.getDimension();
-
-          switch( dim0 )
-            {
-            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
-            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
-            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
-            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
-            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
-            }
-          }
-        }
-
-      mInter[1][mNumEffects] = null;
-
-      if( me.mStatic1 != null )
-        {
-        Static s = me.mStatic1;
-        int dim1 = s.getDimension();
-
-        switch( dim1 )
-          {
-          case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 4] = ((Static5D)s).getV();
-          case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 3] = ((Static4D)s).getW();
-          case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 2] = ((Static3D)s).getZ();
-          case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 1] = ((Static2D)s).getY();
-          case 1 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0    ] = ((Static1D)s).getX();
-          }
-        }
-
-      if( me.mCenter instanceof Dynamic3D)
-        {
-        mInter[2][mNumEffects] = (Dynamic3D)me.mCenter;
-        }
-      else if( me.mCenter instanceof Static3D )
-        {
-        mInter[2][mNumEffects] = null;
-
-        Static3D s = (Static3D)me.mCenter;
-        mUniforms[NUM_UNIFORMS*mNumEffects+4] = s.getX();
-        mUniforms[NUM_UNIFORMS*mNumEffects+5] = s.getY();
-        mUniforms[NUM_UNIFORMS*mNumEffects+6] = s.getZ();
-        }
-      else return -1;
-
-      return addBase(me);
-      }
-      
-    return -1;
-    }
-  }
diff --git a/src/main/java/org/distorted/library/effect/EffectQueuePostprocess.java b/src/main/java/org/distorted/library/effect/EffectQueuePostprocess.java
deleted file mode 100644
index 7b995f7..0000000
--- a/src/main/java/org/distorted/library/effect/EffectQueuePostprocess.java
+++ /dev/null
@@ -1,444 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.effect;
-
-import android.content.res.Resources;
-import android.opengl.GLES30;
-
-import org.distorted.library.Distorted;
-import org.distorted.library.DistortedFramebuffer;
-import org.distorted.library.DistortedOutputSurface;
-import org.distorted.library.DistortedRenderState;
-import org.distorted.library.R;
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.program.DistortedProgram;
-import org.distorted.library.program.FragmentCompilationException;
-import org.distorted.library.program.FragmentUniformsException;
-import org.distorted.library.program.LinkingException;
-import org.distorted.library.program.VertexCompilationException;
-import org.distorted.library.program.VertexUniformsException;
-import org.distorted.library.type.Static;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.library.type.Static5D;
-
-import java.io.InputStream;
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.nio.FloatBuffer;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueuePostprocess extends EffectQueue
-  {
-  private static final int MAX_HALO = 50;    // Support effects creating up to MAX_HALO pixels wide 'halo' around the object.
-
-  private static final int POS_DATA_SIZE= 2; // Post Program: size of the position data in elements
-  private static final int TEX_DATA_SIZE= 2; // Post Program: size of the texture coordinate data in elements.
-
-  private static final int NUM_UNIFORMS = 5;
-  private static final int NUM_CACHE    = 0;
-  private static final int INDEX = Effect.POSTPROCESS;
-
-  private static final FloatBuffer mQuadPositions, mQuadTexture, mQuadTextureInv;
-
-  static
-    {
-    int dataLength      = 4;
-    int bytes_per_float = 4;
-
-    float[] position  = { -0.5f, -0.5f,  -0.5f, 0.5f,  0.5f,-0.5f,  0.5f, 0.5f };
-    float[] textureNor= {  0.0f,  0.0f,   0.0f, 1.0f,  1.0f, 0.0f,  1.0f, 1.0f };
-    float[] textureInv= {  0.0f,  0.0f,   1.0f, 0.0f,  0.0f, 1.0f,  1.0f, 1.0f };
-
-    mQuadPositions = ByteBuffer.allocateDirect(POS_DATA_SIZE*dataLength*bytes_per_float).order(ByteOrder.nativeOrder()).asFloatBuffer();
-    mQuadPositions.put(position).position(0);
-    mQuadTexture= ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*bytes_per_float).order(ByteOrder.nativeOrder()).asFloatBuffer();
-    mQuadTexture.put(textureNor).position(0);
-    mQuadTextureInv= ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*bytes_per_float).order(ByteOrder.nativeOrder()).asFloatBuffer();
-    mQuadTextureInv.put(textureInv).position(0);
-    }
-
-  int mQualityLevel;
-  float mQualityScale;
-  private int mHalo;
-
-  /////////////////////////////////////////////////////////////////////////////////
-  // BLUR effect
-  private static final float GAUSSIAN[] =   // G(0.00), G(0.03), G(0.06), ..., G(3.00), 0
-    {                                       // where G(x)= (1/(sqrt(2*PI))) * e^(-(x^2)/2). The last 0 terminates.
-    0.398948f, 0.398769f, 0.398231f, 0.397336f, 0.396086f, 0.394485f, 0.392537f, 0.390247f, 0.387622f, 0.384668f,
-    0.381393f, 0.377806f, 0.373916f, 0.369733f, 0.365268f, 0.360532f, 0.355538f, 0.350297f, 0.344823f, 0.339129f,
-    0.333229f, 0.327138f, 0.320868f, 0.314436f, 0.307856f, 0.301142f, 0.294309f, 0.287373f, 0.280348f, 0.273248f,
-    0.266089f, 0.258884f, 0.251648f, 0.244394f, 0.237135f, 0.229886f, 0.222657f, 0.215461f, 0.208311f, 0.201217f,
-    0.194189f, 0.187238f, 0.180374f, 0.173605f, 0.166940f, 0.160386f, 0.153951f, 0.147641f, 0.141462f, 0.135420f,
-    0.129520f, 0.123765f, 0.118159f, 0.112706f, 0.107408f, 0.102266f, 0.097284f, 0.092461f, 0.087797f, 0.083294f,
-    0.078951f, 0.074767f, 0.070741f, 0.066872f, 0.063158f, 0.059596f, 0.056184f, 0.052920f, 0.049801f, 0.046823f,
-    0.043984f, 0.041280f, 0.038707f, 0.036262f, 0.033941f, 0.031740f, 0.029655f, 0.027682f, 0.025817f, 0.024056f,
-    0.022395f, 0.020830f, 0.019357f, 0.017971f, 0.016670f, 0.015450f, 0.014305f, 0.013234f, 0.012232f, 0.011295f,
-    0.010421f, 0.009606f, 0.008847f, 0.008140f, 0.007483f, 0.006873f, 0.006307f, 0.005782f, 0.005296f, 0.004847f,
-    0.004432f, 0.000000f
-    };
-  private static final int NUM_GAUSSIAN = GAUSSIAN.length-2;
-
-  // The (fixed-function-sampled) Gaussian Blur kernels are of the size k0=1, k1=2, k2=2, k3=3, k4=3, k5=4, k6=4,...
-  // i.e. k(i)=floor((i+3)/2).  (the 'i' in k(i) means 'blur taking into account the present pixel and 'i' pixels
-  // in all 4 directions)
-  // We need room for MAX_BLUR of them, and sum(i=0...N, floor((i+3)/2)) = N + floor(N*N/4)
-  private static float[] weightsCache = new float[MAX_HALO + MAX_HALO*MAX_HALO/4];
-  private static float[] offsetsCache = new float[MAX_HALO + MAX_HALO*MAX_HALO/4];
-
-  private static DistortedProgram mBlur1Program, mBlur2Program;
-  private static int mRadius1H,mOffsets1H,mWeights1H,mDepth1H, mColorTexture1H;
-  private static int mRadius2H,mOffsets2H,mWeights2H,mDepth2H, mColorTexture2H;
-  private static float[] mWeights = new float[MAX_HALO];
-  private static float[] mOffsets = new float[MAX_HALO];
-  /////////////////////////////////////////////////////////////////////////////////
-  // GLOW effect
-
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  EffectQueuePostprocess(long id)
-    { 
-    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX );
-
-    mQualityLevel = 0;
-    mQualityScale = 1.0f;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void createProgram(Resources resources)
-  throws FragmentCompilationException,VertexCompilationException,VertexUniformsException,FragmentUniformsException,LinkingException
-    {
-    final InputStream blur1VertexStream   = resources.openRawResource(R.raw.blur_vertex_shader);
-    final InputStream blur1FragmentStream = resources.openRawResource(R.raw.blur1_fragment_shader);
-
-    try
-      {
-      mBlur1Program = new DistortedProgram(blur1VertexStream,blur1FragmentStream,
-                                          Distorted.GLSL_VERSION,
-                                          Distorted.GLSL_VERSION + "#define MAX_BLUR "+MAX_HALO, Distorted.GLSL);
-      }
-    catch(Exception e)
-      {
-      android.util.Log.e("EFFECTS", "exception trying to compile BLUR1 program: "+e.getMessage());
-      throw new RuntimeException(e.getMessage());
-      }
-
-    int blur1ProgramH = mBlur1Program.getProgramHandle();
-    mRadius1H       = GLES30.glGetUniformLocation( blur1ProgramH, "u_Radius");
-    mOffsets1H      = GLES30.glGetUniformLocation( blur1ProgramH, "u_Offsets");
-    mWeights1H      = GLES30.glGetUniformLocation( blur1ProgramH, "u_Weights");
-    mDepth1H        = GLES30.glGetUniformLocation( blur1ProgramH, "u_Depth");
-    mColorTexture1H = GLES30.glGetUniformLocation( blur1ProgramH, "u_ColorTexture");
-
-    final InputStream blur2VertexStream   = resources.openRawResource(R.raw.blur_vertex_shader);
-    final InputStream blur2FragmentStream = resources.openRawResource(R.raw.blur2_fragment_shader);
-
-    try
-      {
-      mBlur2Program = new DistortedProgram(blur2VertexStream,blur2FragmentStream,
-                                          Distorted.GLSL_VERSION,
-                                          Distorted.GLSL_VERSION + "#define MAX_BLUR "+MAX_HALO, Distorted.GLSL);
-      }
-    catch(Exception e)
-      {
-      android.util.Log.e("EFFECTS", "exception trying to compile BLUR2 program: "+e.getMessage());
-      // run anyway as compiling Blur2 WILL fail on OpenGL 2.0 contexts
-      mBlur2Program = mBlur1Program;
-      }
-
-    int blur2ProgramH = mBlur2Program.getProgramHandle();
-    mRadius2H       = GLES30.glGetUniformLocation( blur2ProgramH, "u_Radius");
-    mOffsets2H      = GLES30.glGetUniformLocation( blur2ProgramH, "u_Offsets");
-    mWeights2H      = GLES30.glGetUniformLocation( blur2ProgramH, "u_Weights");
-    mDepth2H        = GLES30.glGetUniformLocation( blur2ProgramH, "u_Depth");
-    mColorTexture2H = GLES30.glGetUniformLocation( blur2ProgramH, "u_ColorTexture");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean compute(long currTime)
-    {
-    if( currTime==mTime ) return false;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-
-    mHalo = 0;
-    int halo;
-
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-        {
-        for(int j=0; j<mNumListeners; j++)
-          EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                          EffectMessage.EFFECT_FINISHED,
-                                         (mID[i]<<Effect.LENGTH)+Effect.POSTPROCESS,
-                                          mName[i],
-                                          mObjectID);
-
-        if( PostprocessEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
-          {
-          remove(i);
-          i--;
-          continue;
-          }
-        else mInter[0][i] = null;
-        }
-
-      if( mInter[1][i]!=null ) mInter[1][i].interpolateMain( mUniforms, NUM_UNIFORMS*i+1, mCurrentDuration[i], step);
-
-      halo = (int)mUniforms[NUM_UNIFORMS*i];
-      if( halo>mHalo ) mHalo = halo;
-      }
-
-    mTime = currTime;
-
-    return true;
-    }  
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  protected void moveEffect(int index)
-    {
-    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
-    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
-    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
-    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
-    mUniforms[NUM_UNIFORMS*index+4] = mUniforms[NUM_UNIFORMS*(index+1)+4];
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// This implements the 'Better separable implementation using GPU fixed function sampling' from
-// https://software.intel.com/en-us/blogs/2014/07/15/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms
-
-  private void computeGaussianKernel(int radius)
-    {
-    int offset = radius + radius*radius/4;
-
-    if( weightsCache[offset]==0.0f )
-      {
-      float z, x= 0.0f, P= (float)NUM_GAUSSIAN / (radius>3 ? radius:3);
-      mWeights[0] = GAUSSIAN[0];
-      float sum   = GAUSSIAN[0];
-      int j;
-
-      for(int i=1; i<=radius; i++)
-        {
-        x += P;
-        j = (int)x;
-        z = x-j;
-
-        mWeights[i] = (1-z)*GAUSSIAN[j] + z*GAUSSIAN[j+1];
-        sum += 2*mWeights[i];
-        }
-
-      for(int i=0; i<=radius; i++) mWeights[i] /= sum;
-
-      int numloops = radius/2;
-      weightsCache[offset] = mWeights[0];
-      offsetsCache[offset] = 0.0f;
-
-      for(int i=0; i<numloops; i++)
-        {
-        offsetsCache[offset+i+1] = mWeights[2*i+1]*(2*i+1) + mWeights[2*i+2]*(2*i+2);
-        weightsCache[offset+i+1] = mWeights[2*i+1] + mWeights[2*i+2];
-        offsetsCache[offset+i+1]/= weightsCache[offset+i+1];
-        }
-
-      if( radius%2 == 1 )
-        {
-        int index = offset + radius/2 +1;
-        offsetsCache[index]=radius;
-        weightsCache[index]=mWeights[radius];
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getHalo()
-    {
-    return mNumEffects>0 ? mHalo : 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int postprocess(long time, DistortedOutputSurface surface)
-    {
-    int numRenders = 0;
-
-    if( mNumEffects>0 )
-      {
-      compute(time);
-
-      for(int i=0; i<mNumEffects; i++)
-        {
-        if (mName[i] == PostprocessEffect.BLUR )
-          {
-          blur(NUM_UNIFORMS*i,surface);
-          numRenders += 2;
-          }
-        else if (mName[i] == PostprocessEffect.GLOW )
-          {
-          glow(NUM_UNIFORMS*i,surface);
-          numRenders += 2;
-          }
-        }
-      }
-
-    return numRenders;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void blur(int index, DistortedOutputSurface surface)
-    {
-    DistortedFramebuffer buffer = surface.mBuffer[mQualityLevel];
-    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, buffer.mFBOH[0]);
-
-    float w1 = buffer.mWidth;
-    float h1 = buffer.mHeight;
-
-    int radius = (int)(mUniforms[index]*mQualityScale);
-    if( radius>=MAX_HALO ) radius = MAX_HALO-1;
-    computeGaussianKernel(radius);
-
-    int offset = radius + radius*radius/4;
-    radius = (radius+1)/2;
-
-    // horizontal blur
-    GLES30.glViewport(0, 0, (int)w1, (int)h1);
-    mBlur1Program.useProgram();
-    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[1], 0);
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mColorH[0]);
-
-    GLES30.glUniform1fv( mWeights1H, radius+1, weightsCache,offset);
-    GLES30.glUniform1i( mRadius1H, radius);
-    GLES30.glUniform1f( mDepth1H , 1.0f-surface.mNear);
-    GLES30.glUniform1i( mColorTexture1H , 0 );
-    for(int i=0; i<=radius; i++) mOffsets[i] = offsetsCache[offset+i]/h1;
-    GLES30.glUniform1fv( mOffsets1H ,radius+1, mOffsets,0);
-    GLES30.glVertexAttribPointer(mBlur1Program.mAttribute[0], POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadPositions);
-    GLES30.glVertexAttribPointer(mBlur1Program.mAttribute[1], TEX_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadTexture);
-
-    DistortedRenderState.useStencilMark();
-    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
-    DistortedRenderState.unuseStencilMark();
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
-
-    // vertical blur
-    mBlur2Program.useProgram();
-    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[0], 0);
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mColorH[1]);
-
-    GLES30.glColorMask(true,true,true,true);
-    GLES30.glClearColor(0.0f,0.0f,0.0f,0.0f);
-    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
-
-    GLES30.glUniform1fv( mWeights2H, radius+1, weightsCache,offset);
-    GLES30.glUniform1i( mRadius2H, radius);
-    GLES30.glUniform1f( mDepth2H , 1.0f-surface.mNear);
-    GLES30.glUniform1i( mColorTexture2H , 0 );
-    for(int i=0; i<=radius; i++) mOffsets[i] = offsetsCache[offset+i]/w1;
-    GLES30.glUniform1fv( mOffsets2H ,radius+1, mOffsets,0);
-    GLES30.glVertexAttribPointer(mBlur2Program.mAttribute[0], POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadPositions);
-    GLES30.glVertexAttribPointer(mBlur2Program.mAttribute[1], TEX_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadTexture);
-
-    DistortedRenderState.useStencilMark();
-    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
-    DistortedRenderState.unuseStencilMark();
-    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
-    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void glow(int index, DistortedOutputSurface surface)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized long add(PostprocessEffect pe)
-    {
-    if( mMax[INDEX]>mNumEffects )
-      {
-      int dim0 = 0;
-
-      if( pe.mDynamic0 != null )
-        {
-        mInter[0][mNumEffects] = pe.mDynamic0;
-        dim0 = pe.mDynamic0.getDimension();
-        }
-      else
-        {
-        mInter[0][mNumEffects] = null;
-
-        if( pe.mStatic0 != null )
-          {
-          Static s = pe.mStatic0;
-          dim0 = s.getDimension();
-
-          switch( dim0 )
-            {
-            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
-            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
-            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
-            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
-            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
-            }
-          }
-        }
-
-      mInter[1][mNumEffects] = null;
-
-      if( pe.mStatic1 != null )
-        {
-        Static s = pe.mStatic1;
-        int dim1 = s.getDimension();
-
-        switch( dim1 )
-          {
-          case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 4] = ((Static5D)s).getV();
-          case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 3] = ((Static4D)s).getW();
-          case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 2] = ((Static3D)s).getZ();
-          case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 1] = ((Static2D)s).getY();
-          case 1 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0    ] = ((Static1D)s).getX();
-          }
-        }
-
-      return addBase(pe);
-      }
-
-    return -1;
-    }
-  }
diff --git a/src/main/java/org/distorted/library/effect/EffectQueueVertex.java b/src/main/java/org/distorted/library/effect/EffectQueueVertex.java
deleted file mode 100644
index 2c6e5c3..0000000
--- a/src/main/java/org/distorted/library/effect/EffectQueueVertex.java
+++ /dev/null
@@ -1,255 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2016 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Distorted.                                                               //
-//                                                                                               //
-// Distorted 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 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.library.effect;
-
-import android.opengl.GLES30;
-
-import org.distorted.library.message.EffectMessage;
-import org.distorted.library.type.Dynamic3D;
-import org.distorted.library.type.Dynamic4D;
-import org.distorted.library.type.Static;
-import org.distorted.library.type.Static1D;
-import org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static3D;
-import org.distorted.library.type.Static4D;
-import org.distorted.library.type.Static5D;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-class EffectQueueVertex extends EffectQueue
-  { 
-  private static final int NUM_UNIFORMS = 12;
-  private static final int NUM_CACHE    =  3;
-  private static final int INDEX = Effect.VERTEX;
-  private static int mNumEffectsH;
-  private static int mTypeH;
-  private static int mUniformsH;
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-   
-  EffectQueueVertex(long id)
-    { 
-    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  static void getUniforms(int mProgramH)
-    {
-    mNumEffectsH= GLES30.glGetUniformLocation( mProgramH, "vNumEffects");
-    mTypeH      = GLES30.glGetUniformLocation( mProgramH, "vType");
-    mUniformsH  = GLES30.glGetUniformLocation( mProgramH, "vUniforms");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-  
-  synchronized void compute(long currTime) 
-    {
-    if( currTime==mTime ) return;
-    if( mTime==0 ) mTime = currTime;
-    long step = (currTime-mTime);
-   
-    for(int i=0; i<mNumEffects; i++)
-      {
-      mCurrentDuration[i] += step;
-
-      if( mInter[0][i]!=null )
-        {
-        if( mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
-          {
-          postprocess(i);
-
-          for(int j=0; j<mNumListeners; j++)
-            EffectMessageSender.newMessage( mListeners.elementAt(j),
-                                            EffectMessage.EFFECT_FINISHED,
-                                           (mID[i]<<Effect.LENGTH)+Effect.VERTEX,
-                                            mName[i],
-                                            mObjectID);
-
-          if( VertexEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
-            {
-            remove(i);
-            i--;
-            continue;
-            }
-          else mInter[0][i] = null;
-          }
-        else
-          {
-          postprocess(i);
-          }
-        }
-
-      if( mInter[1][i]!=null ) mInter[1][i].interpolateMain(mUniforms, NUM_UNIFORMS*i+8, mCurrentDuration[i], step);
-      if( mInter[2][i]!=null ) mInter[2][i].interpolateMain(mCache   , NUM_CACHE*i     , mCurrentDuration[i], step);
-      }
-     
-    mTime = currTime;  
-    }  
-  
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  protected void moveEffect(int index)
-    {
-    mUniforms[NUM_UNIFORMS*index   ] = mUniforms[NUM_UNIFORMS*(index+1)   ];
-    mUniforms[NUM_UNIFORMS*index+ 1] = mUniforms[NUM_UNIFORMS*(index+1)+ 1];
-    mUniforms[NUM_UNIFORMS*index+ 2] = mUniforms[NUM_UNIFORMS*(index+1)+ 2];
-    mUniforms[NUM_UNIFORMS*index+ 3] = mUniforms[NUM_UNIFORMS*(index+1)+ 3];
-    mUniforms[NUM_UNIFORMS*index+ 4] = mUniforms[NUM_UNIFORMS*(index+1)+ 4];
-
-    mCache[NUM_CACHE*index  ] = mCache[NUM_CACHE*(index+1)  ];
-    mCache[NUM_CACHE*index+1] = mCache[NUM_CACHE*(index+1)+1];
-    mCache[NUM_CACHE*index+2] = mCache[NUM_CACHE*(index+1)+2];
-
-    mUniforms[NUM_UNIFORMS*index+ 8] = mUniforms[NUM_UNIFORMS*(index+1)+ 8];
-    mUniforms[NUM_UNIFORMS*index+ 9] = mUniforms[NUM_UNIFORMS*(index+1)+ 9];
-    mUniforms[NUM_UNIFORMS*index+10] = mUniforms[NUM_UNIFORMS*(index+1)+10];
-    mUniforms[NUM_UNIFORMS*index+11] = mUniforms[NUM_UNIFORMS*(index+1)+11];
-    }
-   
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized void send(float halfX, float halfY, float halfZ)
-    {
-    GLES30.glUniform1i( mNumEffectsH, mNumEffects);
-      
-    if( mNumEffects>0 )
-      {
-      for(int i=0; i<mNumEffects; i++)
-        {
-        mUniforms[NUM_UNIFORMS*i+5] = mCache[NUM_CACHE*i  ]-halfX;
-        mUniforms[NUM_UNIFORMS*i+6] =-mCache[NUM_CACHE*i+1]+halfY;
-        mUniforms[NUM_UNIFORMS*i+7] = mCache[NUM_CACHE*i+2]-halfZ;
-        }
-
-      GLES30.glUniform1iv( mTypeH    ,                 mNumEffects, mName    ,0);
-      GLES30.glUniform4fv( mUniformsH,(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Do various post-processing on already computed effects.
-// 1) here unlike in the fragment queue, we don't have to multiply the points by ModelView matrix because that gets done in the shader.
-// 2) in case of SWIRL, switch the angles from degrees to radians
-// 3) likewise in case of WAVE and PINCH
-// 4) In case of DISTORT, invert the Y-axis
-  
-  private void postprocess(int effect)
-    {
-    if( mName[effect]==VertexEffect.SWIRL )
-      {
-      mUniforms[NUM_UNIFORMS*effect  ] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect  ]/180);
-      }
-    if( mName[effect]==VertexEffect.PINCH )
-      {
-      mUniforms[NUM_UNIFORMS*effect+1] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+1]/180);
-      }
-    if( mName[effect]==VertexEffect.WAVE )
-      {
-      mUniforms[NUM_UNIFORMS*effect+2] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+2]/180);
-      mUniforms[NUM_UNIFORMS*effect+3] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+3]/180);
-      mUniforms[NUM_UNIFORMS*effect+4] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+4]/180);
-      }
-    if( mName[effect]==VertexEffect.DISTORT )
-      {
-      mUniforms[NUM_UNIFORMS*effect+1] =-mUniforms[NUM_UNIFORMS*effect+1];
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized long add(VertexEffect ve)
-    {
-    if( mMax[INDEX]>mNumEffects )
-      {
-      if( ve.mDynamic0 != null )
-        {
-        mInter[0][mNumEffects] = ve.mDynamic0;
-        }
-      else
-        {
-        mInter[0][mNumEffects] = null;
-
-        if( ve.mStatic0 != null )
-          {
-          Static s = ve.mStatic0;
-
-          switch( s.getDimension() )
-            {
-            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
-            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
-            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
-            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
-            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
-            }
-          }
-        }
-
-      if( ve.mRegion!=null )
-        {
-        if( ve.mRegion instanceof Dynamic4D)
-          {
-          mInter[1][mNumEffects] = (Dynamic4D)ve.mRegion;
-          }
-        else if ( ve.mRegion instanceof Static4D)
-          {
-          Static4D tmp = (Static4D)ve.mRegion;
-
-          float z = tmp.getZ();
-
-          mUniforms[NUM_UNIFORMS*mNumEffects+ 8] = tmp.getX();
-          mUniforms[NUM_UNIFORMS*mNumEffects+ 9] =-tmp.getY();   // invert y already
-          mUniforms[NUM_UNIFORMS*mNumEffects+10] = z<=0.0f ? Float.MAX_VALUE : z;
-          mUniforms[NUM_UNIFORMS*mNumEffects+11] = tmp.getW();
-          mInter[1][mNumEffects] = null;
-          }
-        else return -1;
-        }
-      else
-        {
-        mUniforms[NUM_UNIFORMS*mNumEffects+ 8] = 0.0f;
-        mUniforms[NUM_UNIFORMS*mNumEffects+ 9] = 0.0f;
-        mUniforms[NUM_UNIFORMS*mNumEffects+10] = Float.MAX_VALUE;
-        mUniforms[NUM_UNIFORMS*mNumEffects+11] = 0.0f;
-        mInter[1][mNumEffects] = null;
-        }
-
-      if( ve.mCenter instanceof Dynamic3D)
-        {
-        mInter[2][mNumEffects] = (Dynamic3D)ve.mCenter;
-        }
-      else if( ve.mCenter instanceof Static3D)
-        {
-        mInter[2][mNumEffects] = null;
-        mCache[NUM_CACHE*mNumEffects  ] = ((Static3D)ve.mCenter).getX();
-        mCache[NUM_CACHE*mNumEffects+1] = ((Static3D)ve.mCenter).getY();
-        mCache[NUM_CACHE*mNumEffects+2] = ((Static3D)ve.mCenter).getZ();
-        }
-
-      long ret= addBase(ve);
-
-      postprocess(mNumEffects-1); //addBase just incremented mNumEffects
-
-      return ret;
-      }
-
-    return -1;
-    }
-  }
diff --git a/src/main/java/org/distorted/library/main/Distorted.java b/src/main/java/org/distorted/library/main/Distorted.java
new file mode 100644
index 0000000..fe5e422
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/Distorted.java
@@ -0,0 +1,157 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.content.res.Resources;
+
+import org.distorted.library.program.*;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * A singleton class used to control various global settings.
+ */
+public class Distorted 
+  {
+  static int GLSL;
+  static String GLSL_VERSION;
+  /**
+   * When creating an instance of a DistortedTexture from another instance, clone the Bitmap that's
+   * backing up our DistortedTexture.
+   * <p>
+   * This way we can have two DistortedTextures, both backed up by the same Bitmap, to which we can
+   * apply different effects. Used in the copy constructor.
+   */
+  public static final int CLONE_SURFACE = 0x1;
+  /**
+   * When creating an instance of a DistortedEffects from another instance, clone the Matrix Effects.
+   * <p>
+   * This way we can have two different DistortedEffects sharing the MATRIX queue.
+   */
+  public static final int CLONE_MATRIX = 0x2;
+  /**
+   * When creating an instance of a DistortedEffects from another instance, clone the Vertex Effects.
+   * <p>
+   * This way we can have two different DistortedEffects sharing the VERTEX queue.
+   */
+  public static final int CLONE_VERTEX  = 0x4;
+  /**
+   * When creating an instance of a DistortedEffects from another instance, clone the Fragment Effects.
+   * <p>
+   * This way we can have two different DistortedEffects sharing the FRAGMENT queue.
+   */
+  public static final int CLONE_FRAGMENT= 0x8;
+   /**
+   * When creating an instance of a DistortedEffects from another instance, clone the PostProcess Effects.
+   * <p>
+   * This way we can have two different DistortedEffects sharing the POSTPROCESS queue.
+   */
+  public static final int CLONE_POSTPROCESS= 0x10;
+  /**
+   * When creating an instance of a DistortedNode from another instance, clone the children Nodes.
+   * <p>
+   * This is mainly useful for creating many similar sub-trees and rendering then at different places
+   * on the screen with (optionally) different Effects.
+   */
+  public static final int CLONE_CHILDREN= 0x20;
+
+  private static boolean mInitialized=false;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// private: hide this from Javadoc
+
+  private Distorted()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Have we called onCreate yet, ie have we initialized the library?
+ * @return <code>true</code> if the library is initilized and ready for action.
+ */
+  public static boolean isInitialized()
+    {
+    return mInitialized;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When OpenGL context gets created, you need to call this method so that the library can initialise its internal data structures.
+ * I.e. best called from GLSurfaceView.onCreate().
+ * <p>
+ * Needs to be called from a thread holding the OpenGL context.
+ *   
+ * @param context Context of the App using the library - used to open up Resources and read Shader code.
+ * @throws FragmentCompilationException Fragment Shader failed to compile
+ * @throws VertexCompilationException   Vertex Shader failed to compile
+ * @throws VertexUniformsException      Too many uniforms in the Vertex Shader
+ * @throws FragmentUniformsException    Too many uniforms in the Fragment Shader
+ * @throws LinkingException             Shader failed to link
+ */
+  public static void onCreate(final Context context)
+  throws FragmentCompilationException,VertexCompilationException,VertexUniformsException,FragmentUniformsException,LinkingException
+    {
+    final ActivityManager activityManager     = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+    final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+    android.util.Log.e("DISTORTED", "Using OpenGL ES "+configurationInfo.getGlEsVersion());
+
+    GLSL = ( (configurationInfo.reqGlEsVersion>>16)>=3 ? 300 : 100 );
+    GLSL_VERSION= (GLSL==100 ? "#version 100\n" : "#version 300 es\n");
+
+    final Resources resources = context.getResources();
+    DistortedEffects.createProgram(resources);
+    EffectQueuePostprocess.createProgram(resources);
+    EffectMessageSender.startSending();
+
+    mInitialized = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Call this so that the Library can release its internal data structures.
+ * Must be called from Activity.onPause().
+ */
+  public static void onPause()
+    {
+    DistortedObject.onPause();
+    DistortedNode.onPause();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Call this so that the Library can release its internal data structures.
+ * Must be called from Activity.onDestroy(). 
+ */
+  public static void onDestroy()
+    {
+    DistortedObject.onDestroy();
+    DistortedNode.onDestroy();
+    DistortedEffects.onDestroy();
+    DistortedEffectsPostprocess.onDestroy();
+    DistortedMaster.onDestroy();
+    EffectQueue.onDestroy();
+    EffectMessageSender.stopSending();
+
+    mInitialized = false;
+    }
+  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/library/main/DistortedEffects.java b/src/main/java/org/distorted/library/main/DistortedEffects.java
new file mode 100644
index 0000000..e3901c7
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedEffects.java
@@ -0,0 +1,1088 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.content.res.Resources;
+import android.opengl.GLES30;
+
+import org.distorted.library.R;
+import org.distorted.library.effect.Effect;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.program.DistortedProgram;
+import org.distorted.library.program.FragmentCompilationException;
+import org.distorted.library.program.FragmentUniformsException;
+import org.distorted.library.program.LinkingException;
+import org.distorted.library.program.VertexCompilationException;
+import org.distorted.library.program.VertexUniformsException;
+import org.distorted.library.type.Data1D;
+import org.distorted.library.type.Data2D;
+import org.distorted.library.type.Data3D;
+import org.distorted.library.type.Data4D;
+import org.distorted.library.type.Data5D;
+import org.distorted.library.type.Static3D;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Class containing Matrix,Vertex and Fragment effect queues. Postprocessing queue is held in a separate
+ * class.
+ * <p>
+ * The queues hold actual effects to be applied to a given (DistortedTexture,MeshObject) combo.
+ */
+public class DistortedEffects
+  {
+  /// MAIN PROGRAM ///
+  private static DistortedProgram mMainProgram;
+  private static int mMainTextureH;
+  private static boolean[] mEffectEnabled = new boolean[EffectNames.size()];
+
+  static
+    {
+    int len = EffectNames.size();
+
+    for(int i=0; i<len; i++)
+      {
+      mEffectEnabled[i] = false;
+      }
+    }
+
+  /// BLIT PROGRAM ///
+  private static DistortedProgram mBlitProgram;
+  private static int mBlitTextureH;
+  private static int mBlitDepthH;
+  private static final FloatBuffer mQuadPositions;
+
+  static
+    {
+    float[] positionData= { -0.5f, -0.5f,  -0.5f, 0.5f,  0.5f,-0.5f,  0.5f, 0.5f };
+    mQuadPositions = ByteBuffer.allocateDirect(32).order(ByteOrder.nativeOrder()).asFloatBuffer();
+    mQuadPositions.put(positionData).position(0);
+    }
+
+  /// BLIT DEPTH PROGRAM ///
+  private static DistortedProgram mBlitDepthProgram;
+  private static int mBlitDepthTextureH;
+  private static int mBlitDepthDepthTextureH;
+  private static int mBlitDepthDepthH;
+
+  /// NORMAL PROGRAM /////
+  private static DistortedProgram mNormalProgram;
+  private static int mNormalMVPMatrixH;
+  /// END PROGRAMS //////
+
+  private static long mNextID =0;
+  private long mID;
+
+  private EffectQueueMatrix mM;
+  private EffectQueueFragment mF;
+  private EffectQueueVertex mV;
+
+  private boolean matrixCloned, vertexCloned, fragmentCloned;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void createProgram(Resources resources)
+  throws FragmentCompilationException,VertexCompilationException,VertexUniformsException,FragmentUniformsException,LinkingException
+    {
+    // MAIN PROGRAM ////////////////////////////////////
+    final InputStream mainVertStream = resources.openRawResource(R.raw.main_vertex_shader);
+    final InputStream mainFragStream = resources.openRawResource(R.raw.main_fragment_shader);
+
+    String mainVertHeader= Distorted.GLSL_VERSION;
+    String mainFragHeader= Distorted.GLSL_VERSION;
+
+    EffectNames name;
+    EffectTypes type;
+    boolean foundF = false;
+    boolean foundV = false;
+
+    for(int i=0; i<mEffectEnabled.length; i++)
+      {
+      if( mEffectEnabled[i] )
+        {
+        name = EffectNames.getName(i);
+        type = EffectNames.getType(i);
+
+        if( type == EffectTypes.VERTEX )
+          {
+          mainVertHeader += ("#define "+name.name()+" "+name.ordinal()+"\n");
+          foundV = true;
+          }
+        else if( type == EffectTypes.FRAGMENT )
+          {
+          mainFragHeader += ("#define "+name.name()+" "+name.ordinal()+"\n");
+          foundF = true;
+          }
+        }
+      }
+
+    mainVertHeader += ("#define NUM_VERTEX "   + ( foundV ? getMaxVertex()   : 0 ) + "\n");
+    mainFragHeader += ("#define NUM_FRAGMENT " + ( foundF ? getMaxFragment() : 0 ) + "\n");
+
+    //android.util.Log.e("Effects", "vertHeader= "+mainVertHeader);
+    //android.util.Log.e("Effects", "fragHeader= "+mainFragHeader);
+
+    String[] feedback = { "v_Position", "v_endPosition" };
+
+    mMainProgram = new DistortedProgram(mainVertStream,mainFragStream, mainVertHeader, mainFragHeader, Distorted.GLSL, feedback);
+
+    int mainProgramH = mMainProgram.getProgramHandle();
+    EffectQueueFragment.getUniforms(mainProgramH);
+    EffectQueueVertex.getUniforms(mainProgramH);
+    EffectQueueMatrix.getUniforms(mainProgramH);
+    mMainTextureH= GLES30.glGetUniformLocation( mainProgramH, "u_Texture");
+
+    // BLIT PROGRAM ////////////////////////////////////
+    final InputStream blitVertStream = resources.openRawResource(R.raw.blit_vertex_shader);
+    final InputStream blitFragStream = resources.openRawResource(R.raw.blit_fragment_shader);
+
+    String blitVertHeader= (Distorted.GLSL_VERSION + "#define NUM_VERTEX 0\n"  );
+    String blitFragHeader= (Distorted.GLSL_VERSION + "#define NUM_FRAGMENT 0\n");
+
+    try
+      {
+      mBlitProgram = new DistortedProgram(blitVertStream,blitFragStream,blitVertHeader,blitFragHeader, Distorted.GLSL);
+      }
+    catch(Exception e)
+      {
+      android.util.Log.e("EFFECTS", "exception trying to compile BLIT program: "+e.getMessage());
+      throw new RuntimeException(e.getMessage());
+      }
+
+    int blitProgramH = mBlitProgram.getProgramHandle();
+    mBlitTextureH  = GLES30.glGetUniformLocation( blitProgramH, "u_Texture");
+    mBlitDepthH    = GLES30.glGetUniformLocation( blitProgramH, "u_Depth");
+
+    // BLIT DEPTH PROGRAM ////////////////////////////////////
+    final InputStream blitDepthVertStream = resources.openRawResource(R.raw.blit_depth_vertex_shader);
+    final InputStream blitDepthFragStream = resources.openRawResource(R.raw.blit_depth_fragment_shader);
+
+    try
+      {
+      mBlitDepthProgram = new DistortedProgram(blitDepthVertStream,blitDepthFragStream,blitVertHeader,blitFragHeader, Distorted.GLSL);
+      }
+    catch(Exception e)
+      {
+      android.util.Log.e("EFFECTS", "exception trying to compile BLIT DEPTH program: "+e.getMessage());
+      throw new RuntimeException(e.getMessage());
+      }
+
+    int blitDepthProgramH   = mBlitDepthProgram.getProgramHandle();
+    mBlitDepthTextureH      = GLES30.glGetUniformLocation( blitDepthProgramH, "u_Texture");
+    mBlitDepthDepthTextureH = GLES30.glGetUniformLocation( blitDepthProgramH, "u_DepthTexture");
+    mBlitDepthDepthH        = GLES30.glGetUniformLocation( blitDepthProgramH, "u_Depth");
+
+    // NORMAL PROGRAM //////////////////////////////////////
+    final InputStream normalVertexStream   = resources.openRawResource(R.raw.normal_vertex_shader);
+    final InputStream normalFragmentStream = resources.openRawResource(R.raw.normal_fragment_shader);
+
+    try
+      {
+      mNormalProgram = new DistortedProgram(normalVertexStream,normalFragmentStream, Distorted.GLSL_VERSION, Distorted.GLSL_VERSION, Distorted.GLSL);
+      }
+    catch(Exception e)
+      {
+      android.util.Log.e("EFFECTS", "exception trying to compile NORMAL program: "+e.getMessage());
+      throw new RuntimeException(e.getMessage());
+      }
+
+    int normalProgramH = mNormalProgram.getProgramHandle();
+    mNormalMVPMatrixH  = GLES30.glGetUniformLocation( normalProgramH, "u_MVPMatrix");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void initializeEffectLists(DistortedEffects d, int flags)
+    {
+    if( (flags & Distorted.CLONE_MATRIX) != 0 )
+      {
+      mM = d.mM;
+      matrixCloned = true;
+      }
+    else
+      {
+      mM = new EffectQueueMatrix(mID);
+      matrixCloned = false;
+      }
+    
+    if( (flags & Distorted.CLONE_VERTEX) != 0 )
+      {
+      mV = d.mV;
+      vertexCloned = true;
+      }
+    else
+      {
+      mV = new EffectQueueVertex(mID);
+      vertexCloned = false;
+      }
+    
+    if( (flags & Distorted.CLONE_FRAGMENT) != 0 )
+      {
+      mF = d.mF;
+      fragmentCloned = true;
+      }
+    else
+      {
+      mF = new EffectQueueFragment(mID);
+      fragmentCloned = false;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void displayNormals(MeshObject mesh)
+    {
+    GLES30.glBindBufferBase(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, 0, mesh.mAttTFO[0]);
+    GLES30.glBeginTransformFeedback( GLES30.GL_POINTS);
+    DistortedRenderState.switchOffDrawing();
+    GLES30.glDrawArrays( GLES30.GL_POINTS, 0, mesh.numVertices);
+    DistortedRenderState.restoreDrawing();
+    GLES30.glEndTransformFeedback();
+    GLES30.glBindBufferBase(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, 0, 0);
+
+    mNormalProgram.useProgram();
+    GLES30.glUniformMatrix4fv(mNormalMVPMatrixH, 1, false, mM.getMVP() , 0);
+    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mesh.mAttTFO[0]);
+    GLES30.glVertexAttribPointer(mNormalProgram.mAttribute[0], MeshObject.POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, 0);
+    GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
+    GLES30.glLineWidth(8.0f);
+    GLES30.glDrawArrays(GLES30.GL_LINES, 0, 2*mesh.numVertices);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void drawPriv(float halfW, float halfH, MeshObject mesh, DistortedOutputSurface surface, long currTime, float marginInPixels)
+    {
+    mM.compute(currTime);
+    mV.compute(currTime);
+    mF.compute(currTime);
+
+    float halfZ = halfW*mesh.zFactor;
+
+    GLES30.glViewport(0, 0, surface.mWidth, surface.mHeight );
+
+    mMainProgram.useProgram();
+    GLES30.glUniform1i(mMainTextureH, 0);
+
+    if( Distorted.GLSL >= 300 )
+      {
+      GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mesh.mAttVBO[0]);
+      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[0], MeshObject.POS_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, MeshObject.OFFSET0);
+      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[1], MeshObject.NOR_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, MeshObject.OFFSET1);
+      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[2], MeshObject.TEX_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, MeshObject.OFFSET2);
+      GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
+      }
+    else
+      {
+      mesh.mVertAttribs.position(0);
+      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[0], MeshObject.POS_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, mesh.mVertAttribs);
+      mesh.mVertAttribs.position(MeshObject.POS_DATA_SIZE);
+      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[1], MeshObject.NOR_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, mesh.mVertAttribs);
+      mesh.mVertAttribs.position(MeshObject.POS_DATA_SIZE+MeshObject.NOR_DATA_SIZE);
+      GLES30.glVertexAttribPointer(mMainProgram.mAttribute[2], MeshObject.TEX_DATA_SIZE, GLES30.GL_FLOAT, false, MeshObject.VERTSIZE, mesh.mVertAttribs);
+      }
+
+    mM.send(surface,halfW,halfH,halfZ,marginInPixels);
+    mV.send(halfW,halfH,halfZ);
+    mF.send(halfW,halfH);
+
+    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, mesh.numVertices);
+
+    if( mesh.mShowNormals ) displayNormals(mesh);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  static void blitPriv(DistortedOutputSurface surface)
+    {
+    mBlitProgram.useProgram();
+
+    GLES30.glViewport(0, 0, surface.mWidth, surface.mHeight );
+    GLES30.glUniform1i(mBlitTextureH, 0);
+    GLES30.glUniform1f( mBlitDepthH , 1.0f-surface.mNear);
+    GLES30.glVertexAttribPointer(mBlitProgram.mAttribute[0], 2, GLES30.GL_FLOAT, false, 0, mQuadPositions);
+    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void blitDepthPriv(DistortedOutputSurface surface)
+    {
+    mBlitDepthProgram.useProgram();
+
+    GLES30.glViewport(0, 0, surface.mWidth, surface.mHeight );
+    GLES30.glUniform1i(mBlitDepthTextureH, 0);
+    GLES30.glUniform1i(mBlitDepthDepthTextureH, 1);
+    GLES30.glUniform1f( mBlitDepthDepthH , 1.0f-surface.mNear);
+    GLES30.glVertexAttribPointer(mBlitDepthProgram.mAttribute[0], 2, GLES30.GL_FLOAT, false, 0, mQuadPositions);
+    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  private void releasePriv()
+    {
+    if( !matrixCloned   ) mM.abortAll(false);
+    if( !vertexCloned   ) mV.abortAll(false);
+    if( !fragmentCloned ) mF.abortAll(false);
+
+    mM = null;
+    mV = null;
+    mF = null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void onDestroy()
+    {
+    mNextID = 0;
+
+    int len = EffectNames.size();
+
+    for(int i=0; i<len; i++)
+      {
+      mEffectEnabled[i] = false;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create empty effect queue.
+ */
+  public DistortedEffects()
+    {
+    mID = ++mNextID;
+    initializeEffectLists(this,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Copy constructor.
+ * <p>
+ * Whatever we do not clone gets created just like in the default constructor.
+ *
+ * @param dc    Source object to create our object from
+ * @param flags A bitmask of values specifying what to copy.
+ *              For example, CLONE_VERTEX | CLONE_MATRIX.
+ */
+  public DistortedEffects(DistortedEffects dc, int flags)
+    {
+    mID = ++mNextID;
+    initializeEffectLists(dc,flags);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Releases all resources. After this call, the queue should not be used anymore.
+ */
+  @SuppressWarnings("unused")
+  public synchronized void delete()
+    {
+    releasePriv();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns unique ID of this instance.
+ *
+ * @return ID of the object.
+ */
+  public long getID()
+      {
+      return mID;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Adds the calling class to the list of Listeners that get notified each time some event happens 
+ * to one of the Effects in those queues. Nothing will happen if 'el' is already in the list.
+ * 
+ * @param el A class implementing the EffectListener interface that wants to get notifications.
+ */
+  @SuppressWarnings("unused")
+  public void registerForMessages(EffectListener el)
+    {
+    mV.registerForMessages(el);
+    mF.registerForMessages(el);
+    mM.registerForMessages(el);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes the calling class from the list of Listeners.
+ * 
+ * @param el A class implementing the EffectListener interface that no longer wants to get notifications.
+ */
+  @SuppressWarnings("unused")
+  public void deregisterForMessages(EffectListener el)
+    {
+    mV.deregisterForMessages(el);
+    mF.deregisterForMessages(el);
+    mM.deregisterForMessages(el);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Aborts all Effects.
+ * @return Number of effects aborted.
+ */
+  public int abortAllEffects()
+    {
+    return mM.abortAll(true) + mV.abortAll(true) + mF.abortAll(true);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Aborts all Effects of a given type, for example all MATRIX Effects.
+ * 
+ * @param type one of the constants defined in {@link EffectTypes}
+ * @return Number of effects aborted.
+ */
+  public int abortEffects(EffectTypes type)
+    {
+    switch(type)
+      {
+      case MATRIX     : return mM.abortAll(true);
+      case VERTEX     : return mV.abortAll(true);
+      case FRAGMENT   : return mF.abortAll(true);
+      default         : return 0;
+      }
+    }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Aborts a single Effect.
+ * 
+ * @param id ID of the Effect we want to abort.
+ * @return number of Effects aborted. Always either 0 or 1.
+ */
+  public int abortEffect(long id)
+    {
+    int type = (int)(id&Effect.MASK);
+
+    if( type==Effect.MATRIX   ) return mM.removeByID(id>>Effect.LENGTH);
+    if( type==Effect.VERTEX   ) return mV.removeByID(id>>Effect.LENGTH);
+    if( type==Effect.FRAGMENT ) return mF.removeByID(id>>Effect.LENGTH);
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Abort all Effects of a given name, for example all rotations.
+ * 
+ * @param name one of the constants defined in {@link EffectNames}
+ * @return number of Effects aborted.
+ */
+  public int abortEffects(EffectNames name)
+    {
+    switch(name.getType())
+      {
+      case MATRIX     : return mM.removeByType(name);
+      case VERTEX     : return mV.removeByType(name);
+      case FRAGMENT   : return mF.removeByType(name);
+      default         : return 0;
+      }
+    }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Print some info about a given Effect to Android's standard out. Used for debugging only.
+ * 
+ * @param id Effect ID we want to print info about
+ * @return <code>true</code> if a single Effect of type effectType has been found.
+ */
+  @SuppressWarnings("unused")
+  public boolean printEffect(long id)
+    {
+    int type = (int)(id&EffectTypes.MASK);
+
+    if( type==EffectTypes.MATRIX.type  )  return mM.printByID(id>>EffectTypes.LENGTH);
+    if( type==EffectTypes.VERTEX.type  )  return mV.printByID(id>>EffectTypes.LENGTH);
+    if( type==EffectTypes.FRAGMENT.type)  return mF.printByID(id>>EffectTypes.LENGTH);
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Enables a given Effect.
+ * <p>
+ * By default, all effects are disabled. One has to explicitly enable each effect one intends to use.
+ * This needs to be called BEFORE shaders get compiled, i.e. before the call to Distorted.onCreate().
+ * The point: by enabling only the effects we need, we can optimize the shaders.
+ *
+ * @param name Name of the Effect to enable.
+ */
+  public static void enableEffect(EffectNames name)
+    {
+    mEffectEnabled[name.ordinal()] = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns the maximum number of Matrix effects.
+ *
+ * @return The maximum number of Matrix effects
+ */
+  @SuppressWarnings("unused")
+  public static int getMaxMatrix()
+    {
+    return EffectQueue.getMax(EffectTypes.MATRIX.ordinal());
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns the maximum number of Vertex effects.
+ *
+ * @return The maximum number of Vertex effects
+ */
+  @SuppressWarnings("unused")
+  public static int getMaxVertex()
+    {
+    return EffectQueue.getMax(EffectTypes.VERTEX.ordinal());
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns the maximum number of Fragment effects.
+ *
+ * @return The maximum number of Fragment effects
+ */
+  @SuppressWarnings("unused")
+  public static int getMaxFragment()
+    {
+    return EffectQueue.getMax(EffectTypes.FRAGMENT.ordinal());
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Sets the maximum number of Matrix effects that can be stored in a single EffectQueue at one time.
+ * This can fail if:
+ * <ul>
+ * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
+ * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
+ *     before the Vertex Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
+ *     time only decreasing the value of 'max' is permitted.
+ * <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
+ * </ul>
+ *
+ * @param max new maximum number of simultaneous Matrix Effects. Has to be a non-negative number not greater
+ *            than Byte.MAX_VALUE
+ * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
+ */
+  @SuppressWarnings("unused")
+  public static boolean setMaxMatrix(int max)
+    {
+    return EffectQueue.setMax(EffectTypes.MATRIX.ordinal(),max);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Sets the maximum number of Vertex effects that can be stored in a single EffectQueue at one time.
+ * This can fail if:
+ * <ul>
+ * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
+ * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
+ *     before the Vertex Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
+ *     time only decreasing the value of 'max' is permitted.
+* <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
+ * </ul>
+ *
+ * @param max new maximum number of simultaneous Vertex Effects. Has to be a non-negative number not greater
+ *            than Byte.MAX_VALUE
+ * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
+ */
+  @SuppressWarnings("unused")
+  public static boolean setMaxVertex(int max)
+    {
+    return EffectQueue.setMax(EffectTypes.VERTEX.ordinal(),max);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Sets the maximum number of Fragment effects that can be stored in a single EffectQueue at one time.
+ * This can fail if:
+ * <ul>
+ * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
+ * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
+ *     before the Fragment Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
+ *     time only decreasing the value of 'max' is permitted.
+ * <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
+ * </ul>
+ *
+ * @param max new maximum number of simultaneous Fragment Effects. Has to be a non-negative number not greater
+ *            than Byte.MAX_VALUE
+ * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
+ */
+  @SuppressWarnings("unused")
+  public static boolean setMaxFragment(int max)
+    {
+    return EffectQueue.setMax(EffectTypes.FRAGMENT.ordinal(),max);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Individual effect functions.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Matrix-based effects
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Moves the Object by a (possibly changing in time) vector.
+ * 
+ * @param vector 3-dimensional Data which at any given time will return a Static3D
+ *               representing the current coordinates of the vector we want to move the Object with.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long move(Data3D vector)
+    {   
+    return mM.add(EffectNames.MOVE,vector);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Scales the Object by (possibly changing in time) 3D scale factors.
+ * 
+ * @param scale 3-dimensional Data which at any given time returns a Static3D
+ *              representing the current x- , y- and z- scale factors.
+ * @return      ID of the effect added, or -1 if we failed to add one.
+ */
+  public long scale(Data3D scale)
+    {   
+    return mM.add(EffectNames.SCALE,scale);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Scales the Object by one uniform, constant factor in all 3 dimensions. Convenience function.
+ *
+ * @param scale The factor to scale all 3 dimensions with.
+ * @return      ID of the effect added, or -1 if we failed to add one.
+ */
+  public long scale(float scale)
+    {
+    return mM.add(EffectNames.SCALE, new Static3D(scale,scale,scale));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Rotates the Object by 'angle' degrees around the center.
+ * Static axis of rotation is given by the last parameter.
+ *
+ * @param angle  Angle that we want to rotate the Object to. Unit: degrees
+ * @param axis   Axis of rotation
+ * @param center Coordinates of the Point we are rotating around.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long rotate(Data1D angle, Static3D axis, Data3D center )
+    {   
+    return mM.add(EffectNames.ROTATE, angle, axis, center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Rotates the Object by 'angle' degrees around the center.
+ * Here both angle and axis can dynamically change.
+ *
+ * @param angleaxis Combined 4-tuple representing the (angle,axisX,axisY,axisZ).
+ * @param center    Coordinates of the Point we are rotating around.
+ * @return          ID of the effect added, or -1 if we failed to add one.
+ */
+  public long rotate(Data4D angleaxis, Data3D center)
+    {
+    return mM.add(EffectNames.ROTATE, angleaxis, center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Rotates the Object by quaternion.
+ *
+ * @param quaternion The quaternion describing the rotation.
+ * @param center     Coordinates of the Point we are rotating around.
+ * @return           ID of the effect added, or -1 if we failed to add one.
+ */
+  public long quaternion(Data4D quaternion, Data3D center )
+    {
+    return mM.add(EffectNames.QUATERNION,quaternion,center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Shears the Object.
+ *
+ * @param shear   The 3-tuple of shear factors. The first controls level
+ *                of shearing in the X-axis, second - Y-axis and the third -
+ *                Z-axis. Each is the tangens of the shear angle, i.e 0 -
+ *                no shear, 1 - shear by 45 degrees (tan(45deg)=1) etc.
+ * @param center  Center of shearing, i.e. the point which stays unmoved.
+ * @return        ID of the effect added, or -1 if we failed to add one.
+ */
+  public long shear(Data3D shear, Data3D center)
+    {
+    return mM.add(EffectNames.SHEAR, shear, center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Fragment-based effects  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes a certain sub-region of the Object smoothly change all three of its RGB components.
+ *        
+ * @param blend  1-dimensional Data that returns the level of blend a given pixel will be
+ *               mixed with the next parameter 'color': pixel = (1-level)*pixel + level*color.
+ *               Valid range: <0,1>
+ * @param color  Color to mix. (1,0,0) is RED.
+ * @param region Region this Effect is limited to.
+ * @param smooth If true, the level of 'blend' will smoothly fade out towards the edges of the region.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long chroma(Data1D blend, Data3D color, Data4D region, boolean smooth)
+    {
+    return mF.add( smooth? EffectNames.SMOOTH_CHROMA:EffectNames.CHROMA, blend, color, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes the whole Object smoothly change all three of its RGB components.
+ *
+ * @param blend  1-dimensional Data that returns the level of blend a given pixel will be
+ *               mixed with the next parameter 'color': pixel = (1-level)*pixel + level*color.
+ *               Valid range: <0,1>
+ * @param color  Color to mix. (1,0,0) is RED.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long chroma(Data1D blend, Data3D color)
+    {
+    return mF.add(EffectNames.CHROMA, blend, color);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes a certain sub-region of the Object smoothly change its transparency level.
+ *        
+ * @param alpha  1-dimensional Data that returns the level of transparency we want to have at any given
+ *               moment: pixel.a *= alpha.
+ *               Valid range: <0,1>
+ * @param region Region this Effect is limited to. 
+ * @param smooth If true, the level of 'alpha' will smoothly fade out towards the edges of the region.
+ * @return       ID of the effect added, or -1 if we failed to add one. 
+ */
+  public long alpha(Data1D alpha, Data4D region, boolean smooth)
+    {
+    return mF.add( smooth? EffectNames.SMOOTH_ALPHA:EffectNames.ALPHA, alpha, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes the whole Object smoothly change its transparency level.
+ *
+ * @param alpha  1-dimensional Data that returns the level of transparency we want to have at any
+ *               given moment: pixel.a *= alpha.
+ *               Valid range: <0,1>
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long alpha(Data1D alpha)
+    {
+    return mF.add(EffectNames.ALPHA, alpha);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes a certain sub-region of the Object smoothly change its brightness level.
+ *        
+ * @param brightness 1-dimensional Data that returns the level of brightness we want to have
+ *                   at any given moment. Valid range: <0,infinity)
+ * @param region     Region this Effect is limited to.
+ * @param smooth     If true, the level of 'brightness' will smoothly fade out towards the edges of the region.
+ * @return           ID of the effect added, or -1 if we failed to add one.
+ */
+  public long brightness(Data1D brightness, Data4D region, boolean smooth)
+    {
+    return mF.add( smooth ? EffectNames.SMOOTH_BRIGHTNESS: EffectNames.BRIGHTNESS, brightness, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes the whole Object smoothly change its brightness level.
+ *
+ * @param brightness 1-dimensional Data that returns the level of brightness we want to have
+ *                   at any given moment. Valid range: <0,infinity)
+ * @return           ID of the effect added, or -1 if we failed to add one.
+ */
+  public long brightness(Data1D brightness)
+    {
+    return mF.add(EffectNames.BRIGHTNESS, brightness);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes a certain sub-region of the Object smoothly change its contrast level.
+ *        
+ * @param contrast 1-dimensional Data that returns the level of contrast we want to have
+ *                 at any given moment. Valid range: <0,infinity)
+ * @param region   Region this Effect is limited to.
+ * @param smooth   If true, the level of 'contrast' will smoothly fade out towards the edges of the region.
+ * @return         ID of the effect added, or -1 if we failed to add one.
+ */
+  public long contrast(Data1D contrast, Data4D region, boolean smooth)
+    {
+    return mF.add( smooth ? EffectNames.SMOOTH_CONTRAST:EffectNames.CONTRAST, contrast, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes the whole Object smoothly change its contrast level.
+ *
+ * @param contrast 1-dimensional Data that returns the level of contrast we want to have
+ *                 at any given moment. Valid range: <0,infinity)
+ * @return         ID of the effect added, or -1 if we failed to add one.
+ */
+  public long contrast(Data1D contrast)
+    {
+    return mF.add(EffectNames.CONTRAST, contrast);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes a certain sub-region of the Object smoothly change its saturation level.
+ *        
+ * @param saturation 1-dimensional Data that returns the level of saturation we want to have
+ *                   at any given moment. Valid range: <0,infinity)
+ * @param region     Region this Effect is limited to.
+ * @param smooth     If true, the level of 'saturation' will smoothly fade out towards the edges of the region.
+ * @return           ID of the effect added, or -1 if we failed to add one.
+ */
+  public long saturation(Data1D saturation, Data4D region, boolean smooth)
+    {
+    return mF.add( smooth ? EffectNames.SMOOTH_SATURATION:EffectNames.SATURATION, saturation, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Makes the whole Object smoothly change its saturation level.
+ *
+ * @param saturation 1-dimensional Data that returns the level of saturation we want to have
+ *                   at any given moment. Valid range: <0,infinity)
+ * @return           ID of the effect added, or -1 if we failed to add one.
+ */
+  public long saturation(Data1D saturation)
+    {
+    return mF.add(EffectNames.SATURATION, saturation);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Vertex-based effects  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Distort a (possibly changing in time) part of the Object by a (possibly changing in time) vector of force.
+ *
+ * @param vector 3-dimensional Vector which represents the force the Center of the Effect is
+ *               currently being dragged with.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @param region Region that masks the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long distort(Data3D vector, Data3D center, Data4D region)
+    {  
+    return mV.add(EffectNames.DISTORT, vector, center, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Distort the whole Object by a (possibly changing in time) vector of force.
+ *
+ * @param vector 3-dimensional Vector which represents the force the Center of the Effect is
+ *               currently being dragged with.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long distort(Data3D vector, Data3D center)
+    {
+    return mV.add(EffectNames.DISTORT, vector, center, null);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Deform the shape of the whole Object with a (possibly changing in time) vector of force applied to
+ * a (possibly changing in time) point on the Object.
+ *
+ * @param vector Vector of force that deforms the shape of the whole Object.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @param region Region that masks the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long deform(Data3D vector, Data3D center, Data4D region)
+    {
+    return mV.add(EffectNames.DEFORM, vector, center, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Deform the shape of the whole Object with a (possibly changing in time) vector of force applied to
+ * a (possibly changing in time) point on the Object.
+ *     
+ * @param vector Vector of force that deforms the shape of the whole Object.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long deform(Data3D vector, Data3D center)
+    {  
+    return mV.add(EffectNames.DEFORM, vector, center, null);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////  
+/**
+ * Pull all points around the center of the Effect towards the center (if degree>=1) or push them
+ * away from the center (degree<=1)
+ *
+ * @param sink   The current degree of the Effect.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @param region Region that masks the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long sink(Data1D sink, Data3D center, Data4D region)
+    {
+    return mV.add(EffectNames.SINK, sink, center, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Pull all points around the center of the Effect towards the center (if degree>=1) or push them
+ * away from the center (degree<=1)
+ *
+ * @param sink   The current degree of the Effect.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long sink(Data1D sink, Data3D center)
+    {
+    return mV.add(EffectNames.SINK, sink, center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Pull all points around the center of the Effect towards a line passing through the center
+ * (that's if degree>=1) or push them away from the line (degree<=1)
+ *
+ * @param pinch  The current degree of the Effect + angle the line forms with X-axis
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @param region Region that masks the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long pinch(Data2D pinch, Data3D center, Data4D region)
+    {
+    return mV.add(EffectNames.PINCH, pinch, center, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Pull all points around the center of the Effect towards a line passing through the center
+ * (that's if degree>=1) or push them away from the line (degree<=1)
+ *
+ * @param pinch  The current degree of the Effect + angle the line forms with X-axis
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long pinch(Data2D pinch, Data3D center)
+    {
+    return mV.add(EffectNames.PINCH, pinch, center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////  
+/**
+ * Rotate part of the Object around the Center of the Effect by a certain angle.
+ *
+ * @param swirl  The angle of Swirl (in degrees). Positive values swirl clockwise.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @param region Region that masks the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long swirl(Data1D swirl, Data3D center, Data4D region)
+    {    
+    return mV.add(EffectNames.SWIRL, swirl, center, region);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Rotate the whole Object around the Center of the Effect by a certain angle.
+ *
+ * @param swirl  The angle of Swirl (in degrees). Positive values swirl clockwise.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long swirl(Data1D swirl, Data3D center)
+    {
+    return mV.add(EffectNames.SWIRL, swirl, center);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Directional, sinusoidal wave effect.
+ *
+ * @param wave   A 5-dimensional data structure describing the wave: first member is the amplitude,
+ *               second is the wave length, third is the phase (i.e. when phase = PI/2, the sine
+ *               wave at the center does not start from sin(0), but from sin(PI/2) ) and the next two
+ *               describe the 'direction' of the wave.
+ *               <p>
+ *               Wave direction is defined to be a 3D vector of length 1. To define such vectors, we
+ *               need 2 floats: thus the third member is the angle Alpha (in degrees) which the vector
+ *               forms with the XY-plane, and the fourth is the angle Beta (again in degrees) which
+ *               the projection of the vector to the XY-plane forms with the Y-axis (counterclockwise).
+ *               <p>
+ *               <p>
+ *               Example1: if Alpha = 90, Beta = 90, (then V=(0,0,1) ) and the wave acts 'vertically'
+ *               in the X-direction, i.e. cross-sections of the resulting surface with the XZ-plane
+ *               will be sine shapes.
+ *               <p>
+ *               Example2: if Alpha = 90, Beta = 0, the again V=(0,0,1) and the wave is 'vertical',
+ *               but this time it waves in the Y-direction, i.e. cross sections of the surface and the
+ *               YZ-plane with be sine shapes.
+ *               <p>
+ *               Example3: if Alpha = 0 and Beta = 45, then V=(sqrt(2)/2, -sqrt(2)/2, 0) and the wave
+ *               is entirely 'horizontal' and moves point (x,y,0) in direction V by whatever is the
+ *               value if sin at this point.
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long wave(Data5D wave, Data3D center)
+    {
+    return mV.add(EffectNames.WAVE, wave, center, null);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Directional, sinusoidal wave effect.
+ *
+ * @param wave   see {@link DistortedEffects#wave(Data5D,Data3D)}
+ * @param center 3-dimensional Data that, at any given time, returns the Center of the Effect.
+ * @param region Region that masks the Effect.
+ * @return       ID of the effect added, or -1 if we failed to add one.
+ */
+  public long wave(Data5D wave, Data3D center, Data4D region)
+    {
+    return mV.add(EffectNames.WAVE, wave, center, region);
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/DistortedEffectsPostprocess.java b/src/main/java/org/distorted/library/main/DistortedEffectsPostprocess.java
new file mode 100644
index 0000000..9201c48
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedEffectsPostprocess.java
@@ -0,0 +1,390 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Data1D;
+import org.distorted.library.type.Data4D;
+
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Class containing the queue of postprocessing effects.
+ * <p>
+ * This better be separate from the main DistortedEffects class, because we want to be able to apply
+ * the same postprocessing effects (i.e. not only the same effects, but the very same instance of this
+ * object) to several Children of a Node. Reason for that: if several children have the same Object,
+ * it is easy to group them into 'Buckets' where each bucket has the same postprocessing effects and
+ * is therefore rendered to the same buffer, which is then postprocessed in one go.
+ * <p>
+ * The queue holds actual effects to be applied to a given bucket of several (DistortedTexture,MeshObject) combos.
+ */
+public class DistortedEffectsPostprocess implements DistortedSlave
+  {
+  private static final int MIPMAP = 0;
+
+  private static long mNextID =0;
+  private long mID;
+
+  private EffectQueuePostprocess mP;
+
+  private boolean postprocessCloned;
+
+  private class Job
+    {
+    int type;
+    int level;
+
+    Job(int t, int l)
+      {
+      type = t;
+      level= l;
+      }
+    }
+
+  private ArrayList<Job> mJobs = new ArrayList<>();
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void initializeEffectLists(DistortedEffectsPostprocess d, int flags)
+    {
+    if( (flags & Distorted.CLONE_POSTPROCESS) != 0 )
+      {
+      mP = d.mP;
+      postprocessCloned = true;
+      }
+    else
+      {
+      mP = new EffectQueuePostprocess(mID);
+      postprocessCloned = false;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int postprocess(long time, DistortedOutputSurface surface)
+    {
+    return mP.postprocess(time,surface);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  long getBucket()
+    {
+    return mP.mNumEffects==0 ? 0: mID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void releasePriv()
+    {
+    if( !postprocessCloned) mP.abortAll(false);
+
+    mP = null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getQuality()
+    {
+    return mP.mQualityLevel;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getHalo()
+    {
+    return mP.getHalo();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void onDestroy()
+    {
+    mNextID = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create empty effect queue.
+ */
+  public DistortedEffectsPostprocess()
+    {
+    mID = ++mNextID;
+    initializeEffectLists(this,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Copy constructor.
+ * <p>
+ * Whatever we do not clone gets created just like in the default constructor.
+ *
+ * @param dc    Source object to create our object from
+ * @param flags A bitmask of values specifying what to copy.
+ *              Currently the only values possible are CLONE_NOTHING or CLONE_POSTPROCESS.
+ */
+  public DistortedEffectsPostprocess(DistortedEffectsPostprocess dc, int flags)
+    {
+    mID = ++mNextID;
+    initializeEffectLists(dc,flags);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * This is not really part of the public API. Has to be public only because it is a part of the
+ * DistortedSlave interface, which should really be a class that we extend here instead but
+ * Java has no multiple inheritance.
+ *
+ * @y.exclude
+ */
+  public void doWork()
+    {
+    int num = mJobs.size();
+    Job job;
+
+    for(int i=0; i<num; i++)
+      {
+      job = mJobs.remove(0);
+
+      switch(job.type)
+        {
+        case MIPMAP: int level = job.level;
+                     mP.mQualityLevel = level;
+                     mP.mQualityScale = 1.0f;
+                     for(int j=0; j<level; j++) mP.mQualityScale*=EffectQuality.MULTIPLIER;
+                     break;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Releases all resources. After this call, the queue should not be used anymore.
+ */
+  @SuppressWarnings("unused")
+  public synchronized void delete()
+    {
+    releasePriv();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns unique ID of this instance.
+ *
+ * @return ID of the object.
+ */
+  @SuppressWarnings("unused")
+  public long getID()
+      {
+      return mID;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Adds the calling class to the list of Listeners that get notified each time some event happens 
+ * to one of the Effects in the queues. Nothing will happen if 'el' is already in the list.
+ * 
+ * @param el A class implementing the EffectListener interface that wants to get notifications.
+ */
+  @SuppressWarnings("unused")
+  public void registerForMessages(EffectListener el)
+    {
+    mP.registerForMessages(el);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes the calling class from the list of Listeners.
+ * 
+ * @param el A class implementing the EffectListener interface that no longer wants to get notifications.
+ */
+  @SuppressWarnings("unused")
+  public void deregisterForMessages(EffectListener el)
+    {
+    mP.deregisterForMessages(el);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Aborts all Effects.
+ * @return Number of effects aborted.
+ */
+  @SuppressWarnings("unused")
+  public int abortAllEffects()
+      {
+      return mP.abortAll(true);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Aborts all Effects of a given type (currently only POSTPROCESSING Effects).
+ * 
+ * @param type one of the constants defined in {@link org.distorted.library.effect.Effect}
+ * @return Number of effects aborted.
+ */
+  @SuppressWarnings("unused")
+  public int abortEffects(int type)
+    {
+    switch(type)
+      {
+      case Effect.POSTPROCESS: return mP.abortAll(true);
+      default                : return 0;
+      }
+    }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Aborts a single Effect.
+ * 
+ * @param id ID of the Effect we want to abort.
+ * @return number of Effects aborted. Always either 0 or 1.
+ */
+  @SuppressWarnings("unused")
+  public int abortEffect(long id)
+    {
+    int type = (int)(id&Effect.MASK);
+
+    if( type==Effect.POSTPROCESS ) return mP.removeByID(id>>Effect.LENGTH);
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Abort all Effects of a given name, for example all blurs.
+ * 
+ * @param name one of the constants defined in {@link org.distorted.library.effect.PostprocessEffect}
+ * @return number of Effects aborted.
+ */
+  @SuppressWarnings("unused")
+  public int abortEffects(int name)
+    {
+    switch(name.getType())
+      {
+      case POSTPROCESS: return mP.removeByType(name);
+      default         : return 0;
+      }
+    }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Print some info about a given Effect to Android's standard out. Used for debugging only.
+ * 
+ * @param id Effect ID we want to print info about
+ * @return <code>true</code> if a single Effect of type effectType has been found.
+ */
+  @SuppressWarnings("unused")
+  public boolean printEffect(long id)
+    {
+    int type = (int)(id&EffectTypes.MASK);
+
+    if( type==EffectTypes.POSTPROCESS.type )  return mP.printByID(id>>EffectTypes.LENGTH);
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns the maximum number of Postprocess effects.
+ *
+ * @return The maximum number of Postprocess effects
+ */
+  @SuppressWarnings("unused")
+  public static int getMaxPostprocess()
+    {
+    return EffectQueue.getMax(EffectTypes.POSTPROCESS.ordinal());
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Sets the maximum number of Postprocess effects that can be stored in a single EffectQueue at one time.
+ * This can fail if:
+ * <ul>
+ * <li>the value of 'max' is outside permitted range (0 &le; max &le; Byte.MAX_VALUE)
+ * <li>We try to increase the value of 'max' when it is too late to do so already. It needs to be called
+ *     before the Fragment Shader gets compiled, i.e. before the call to {@link Distorted#onCreate}. After this
+ *     time only decreasing the value of 'max' is permitted.
+ * <li>Furthermore, this needs to be called before any instances of the DistortedEffects class get created.
+ * </ul>
+ *
+ * @param max new maximum number of simultaneous Postprocess Effects. Has to be a non-negative number not greater
+ *            than Byte.MAX_VALUE
+ * @return <code>true</code> if operation was successful, <code>false</code> otherwise.
+ */
+  @SuppressWarnings("unused")
+  public static boolean setMaxPostprocess(int max)
+    {
+    return EffectQueue.setMax(EffectTypes.POSTPROCESS.ordinal(),max);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * The higher the quality, the better the effect will look like and the slower it will be.
+ * <p>
+ * This works by rendering into smaller and smaller intermediate buffers. Each step renders into a
+ * buffer that's half the size of the previous one.
+ * <p>
+ * We cannot this during mid-render - thus, give it to the Master to assign us back this job on the
+ * next render.
+ */
+  public void setQuality(EffectQuality quality)
+    {
+    mJobs.add(new Job(MIPMAP,quality.level));
+    DistortedMaster.newSlave(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Individual effect functions.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Postprocess-based effects
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Blur the object.
+ *
+ * @param radius The 'strength' if the effect, in pixels. 0 = no blur, 10 = when blurring a given pixel,
+ *               take into account 10 pixels in each direction.
+ * @return ID of the effect added, or -1 if we failed to add one.
+ */
+  public long blur(Data1D radius)
+    {
+    return mP.add(EffectNames.BLUR, radius);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Make the object glow with a specific color and a halo of specific radius.
+ *
+ * @param radius The 'strength' if the effect, in pixels. 0 = no halo, 10 = halo of roughly 10 pixels
+ *               around the whole object.
+ * @param color  RGBA of the color with which to draw the glow.
+ * @return ID of the effect added, or -1 if we failed to add one.
+ */
+  public long glow(Data1D radius, Data4D color)
+    {
+    return mP.add(EffectNames.GLOW, radius, color);
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/DistortedFramebuffer.java b/src/main/java/org/distorted/library/main/DistortedFramebuffer.java
new file mode 100644
index 0000000..b35686f
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedFramebuffer.java
@@ -0,0 +1,268 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.opengl.GLES30;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Class which represents a OpenGL Framebuffer object.
+ * <p>
+ * User is able to create offscreen FBOs and both a) render to them b) use their COLOR0 attachment as
+ * an input texture. Attaching Depths and/or Stencils is also possible.
+ */
+public class DistortedFramebuffer extends DistortedOutputSurface implements DistortedInputSurface
+  {
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void prepareDebug(long time) {}
+  void renderDebug(long time)  {}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Must be called from a thread holding OpenGL Context
+
+  void create()
+    {
+    if( mColorCreated==NOT_CREATED_YET )
+      {
+      GLES30.glGenTextures( mNumColors, mColorH, 0);
+      GLES30.glGenFramebuffers(1, mFBOH, 0);
+      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
+
+      for(int i=0; i<mNumColors; i++)
+        {
+        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[i]);
+        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT);
+        GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT);
+        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
+        GLES30.glTexParameterf(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR);
+        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_RGBA, mWidth, mHeight, 0, GLES30.GL_RGBA, GLES30.GL_UNSIGNED_BYTE, null);
+        }
+      GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mColorH[0], 0);
+      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
+      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);
+
+      mColorCreated = checkStatus("color");
+      }
+    if( mDepthStencilCreated==NOT_CREATED_YET ) // we need to create a new DEPTH or STENCIL attachment
+      {
+      GLES30.glGenTextures(1, mDepthStencilH, 0);
+      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mDepthStencilH[0]);
+      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_REPEAT);
+      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_REPEAT);
+      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_NEAREST);
+      GLES30.glTexParameteri(GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_NEAREST);
+
+      if( mDepthStencil==DEPTH_NO_STENCIL )
+        {
+        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_DEPTH_COMPONENT, mWidth, mHeight, 0, GLES30.GL_DEPTH_COMPONENT, GLES30.GL_UNSIGNED_INT, null);
+        }
+      else if( mDepthStencil==BOTH_DEPTH_STENCIL )
+        {
+        GLES30.glTexImage2D(GLES30.GL_TEXTURE_2D, 0, GLES30.GL_DEPTH24_STENCIL8, mWidth, mHeight, 0, GLES30.GL_DEPTH_STENCIL, GLES30.GL_UNSIGNED_INT_24_8, null);
+        }
+
+      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
+      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
+
+      if( mDepthStencil==DEPTH_NO_STENCIL )
+        {
+        GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_DEPTH_ATTACHMENT, GLES30.GL_TEXTURE_2D, mDepthStencilH[0], 0);
+        }
+      else if( mDepthStencil==BOTH_DEPTH_STENCIL )
+        {
+        GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_DEPTH_STENCIL_ATTACHMENT, GLES30.GL_TEXTURE_2D, mDepthStencilH[0], 0);
+        }
+
+      GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, 0);
+
+      mDepthStencilCreated = checkStatus("depth");
+      }
+    if( mDepthStencilCreated==DONT_CREATE && mDepthStencilH[0]>0 ) // we need to detach and recreate the DEPTH attachment.
+      {
+      GLES30.glDeleteTextures(1, mDepthStencilH, 0);
+      mDepthStencilH[0]=0;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int checkStatus(String message)
+    {
+    int status = GLES30.glCheckFramebufferStatus(GLES30.GL_FRAMEBUFFER);
+
+    if(status != GLES30.GL_FRAMEBUFFER_COMPLETE)
+      {
+      android.util.Log.e("DistortedFramebuffer", "FRAMEBUFFER INCOMPLETE, "+message+" error="+status);
+
+      GLES30.glDeleteTextures(1, mColorH, 0);
+      GLES30.glDeleteTextures(1, mDepthStencilH, 0);
+      GLES30.glDeleteFramebuffers(1, mFBOH, 0);
+      mFBOH[0]= 0;
+
+      return FAILED_TO_CREATE;
+      }
+
+    return CREATED;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Must be called from a thread holding OpenGL Context
+
+  void delete()
+    {
+    if( mColorH[0]>0 )
+      {
+      GLES30.glDeleteTextures(1, mColorH, 0);
+      mColorH[0] = 0;
+      mColorCreated = NOT_CREATED_YET;
+      }
+
+    if( mDepthStencilH[0]>0 )
+      {
+      GLES30.glDeleteTextures(1, mDepthStencilH, 0);
+      mDepthStencilH[0]=0;
+      mDepthStencilCreated = NOT_CREATED_YET;
+      }
+
+    GLES30.glDeleteFramebuffers(1, mFBOH, 0);
+    mFBOH[0] = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// called from onDestroy(); mark OpenGL assets as 'not created'
+
+  void recreate()
+    {
+    if( mColorCreated!=DONT_CREATE )
+      {
+      mColorCreated = NOT_CREATED_YET;
+      mColorH[0] = 0;
+      }
+    if( mDepthStencilCreated!=DONT_CREATE )
+      {
+      mDepthStencilCreated = NOT_CREATED_YET;
+      mDepthStencilH[0] = 0;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// create SYSTEM or TREE framebuffers (those are just like normal FBOs, just hold information
+// that they were autocreated only for the Library's internal purposes (SYSTEM) or for using
+// inside a Tree of DistortedNodes (TREE)
+// SYSTEM surfaces do not get removed in onDestroy().
+
+  DistortedFramebuffer(int numcolors, int depthStencil, int type, int width, int height)
+    {
+    super(width,height,NOT_CREATED_YET,numcolors,depthStencil,NOT_CREATED_YET, type);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create new offscreen Framebuffer with configurable number of COLOR, DEPTH and STENCIL attachments.
+ *
+ * @param width        Width of all the COLOR attachments.
+ * @param height       Height of all the COLOR attachments.
+ * @param numcolors    How many COLOR attachments to create?
+ * @param depthStencil Add DEPTH or STENCIL attachment?
+ *                     Valid values: NO_DEPTH_NO_STENCIL, DEPTH_NO_STENCIL, BOTH_DEPTH_STENCIL.
+ */
+  @SuppressWarnings("unused")
+  public DistortedFramebuffer(int width, int height, int numcolors, int depthStencil)
+    {
+    super(width,height,NOT_CREATED_YET,numcolors,depthStencil,NOT_CREATED_YET,TYPE_USER);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Create new offscreen Framebuffer with COLOR0 attachment only.
+ *
+ * @param width Width of the COLOR0 attachment.
+ * @param height Height of the COLOR0 attachment.
+ */
+  @SuppressWarnings("unused")
+  public DistortedFramebuffer(int width, int height)
+    {
+    super(width,height,NOT_CREATED_YET, 1, NO_DEPTH_NO_STENCIL,NOT_CREATED_YET,TYPE_USER);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Bind the underlying rectangle of pixels as a OpenGL Texture.
+ *
+ * @return <code>true</code> if successful.
+ */
+  public boolean setAsInput()
+    {
+    if( mColorH[0]>0 )
+      {
+      GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Enable.disable DEPTH and STENCIL buffers.
+ *
+ * @param depthStencil Valid values: NO_DEPTH_NO_STENCIL, DEPTH_NO_STENCIL, BOTH_DEPTH_STENCIL.
+ */
+  public void enableDepthStencil(int depthStencil)
+    {
+    if( depthStencil != mDepthStencil )
+      {
+      mDepthStencil = depthStencil;
+
+      if( depthStencil!= NO_DEPTH_NO_STENCIL && mDepthStencilCreated==DONT_CREATE )
+        {
+        mDepthStencilCreated = NOT_CREATED_YET;
+        markForCreation();
+        }
+      if( depthStencil== NO_DEPTH_NO_STENCIL && mDepthStencilCreated!=DONT_CREATE )
+        {
+        mDepthStencilCreated = DONT_CREATE;
+        markForCreation();
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Return the ID of the Texture (COLOR attachment 0) that's backing this FBO.
+ * <p>
+ * Catch: this will only work if the library has had time to actually create the texture. Remember
+ * that the texture gets created only on first render, thus creating a Texture object and immediately
+ * calling this method will return an invalid (negative) result.
+ *
+ * @return If there was not a single render between creation of the Object and calling this method on
+ *         it, return a negative value. Otherwise, return ID of COLOR attachment 0.
+ */
+  public int getTextureID()
+    {
+    return mColorH[0];
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/DistortedInputSurface.java b/src/main/java/org/distorted/library/main/DistortedInputSurface.java
new file mode 100644
index 0000000..f6dcc8e
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedInputSurface.java
@@ -0,0 +1,48 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * A Surface that we can set as an Input (take its rectangle of pixels and skin a Mesh with it).
+ */
+
+public interface DistortedInputSurface
+{
+/**
+ * Do not document this as public API
+ * @y.exclude
+ */
+  long getID();
+/**
+ * Do not document this as public API
+ * @y.exclude
+ */
+  int getWidth();
+/**
+ * Do not document this as public API
+ * @y.exclude
+ */
+  int getHeight();
+/**
+ * Take the underlying rectangle of pixels and bind this texture to OpenGL.
+ */
+  boolean setAsInput();
+}
diff --git a/src/main/java/org/distorted/library/main/DistortedMaster.java b/src/main/java/org/distorted/library/main/DistortedMaster.java
new file mode 100644
index 0000000..4c9ad77
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedMaster.java
@@ -0,0 +1,111 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * This static class handles assigning jobs to other classes. It does it once, at the beginning of
+ * each frame.
+ */
+class DistortedMaster
+  {
+  private static ArrayList<DistortedSlave> mSlaves = new ArrayList<>();
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private DistortedMaster()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static boolean toDo()
+    {
+    DistortedSlave slave;
+    int num = mSlaves.size();
+
+    for(int i=0; i<num; i++)
+      {
+      slave = mSlaves.remove(0);
+      slave.doWork();
+      }
+
+    return ( num>0 );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void newSlave(DistortedSlave s)
+    {
+    int num = mSlaves.size();
+    boolean found = false;
+    DistortedSlave tmp;
+
+    for(int i=0; i<num; i++)
+      {
+      tmp = mSlaves.get(i);
+
+      if( tmp==s )
+        {
+        found = true;
+        break;
+        }
+      }
+
+    if( !found ) mSlaves.add(s);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// can make this logarithmic but the typical number of children is very small anyway
+
+  static void addSorted(ArrayList<DistortedNode> mChildren, DistortedNode newChild)
+    {
+    DistortedNode child;
+    DistortedEffectsPostprocess dep;
+    int i,num = mChildren.size();
+    long bucket, newBucket;
+
+    dep = newChild.getEffectsPostprocess();
+    newBucket = dep==null ? 0 : dep.getBucket();
+
+    for(i=0; i<num; i++)
+      {
+      child = mChildren.get(i);
+      dep   = child.getEffectsPostprocess();
+      bucket= dep==null ? 0 : dep.getBucket();
+
+      if( bucket>newBucket ) break;
+      }
+
+    mChildren.add(i,newChild);
+
+    //android.util.Log.e("newChild", "newBucket="+newBucket+" new child at "+i+" total num ="+num);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void onDestroy()
+    {
+    mSlaves.clear();
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/DistortedNode.java b/src/main/java/org/distorted/library/main/DistortedNode.java
new file mode 100644
index 0000000..08627ee
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedNode.java
@@ -0,0 +1,886 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Class which represents a Node in a Tree of (InputSurface,Mesh,Effects) triplets.
+ * <p>
+ * Having organized such sets into a Tree, we can then render any Node to any OutputSurface.
+ * That recursively renders the set held in the Node and all its children.
+ * <p>
+ * The class takes special care to only render identical sub-trees once. Each Node holds a reference
+ * to sub-class 'NodeData'. Two identical sub-trees attached at different points of the main tree
+ * will point to the same NodeData; only the first of this is rendered (mData.numRender!).
+ */
+public class DistortedNode implements DistortedSlave
+  {
+  private static final int ATTACH = 0;
+  private static final int DETACH = 1;
+  private static final int DETALL = 2;
+  private static final int SORT   = 3;
+
+  private ArrayList<DistortedNode> mChildren;
+  private int[] mNumChildren;  // ==mChildren.length(), but we only create mChildren if the first one gets added
+
+  private class Job
+    {
+    int type;
+    DistortedNode node;
+    DistortedEffectsPostprocess dep;
+
+    Job(int t, DistortedNode n, DistortedEffectsPostprocess d)
+      {
+      type = t;
+      node = n;
+      dep  = d;
+      }
+    }
+
+  private ArrayList<Job> mJobs = new ArrayList<>();
+
+  private static HashMap<ArrayList<Long>,NodeData> mMapNodeID = new HashMap<>();
+  private static long mNextNodeID =0;
+
+  private DistortedNode mParent;
+  private DistortedOutputSurface mSurfaceParent;
+  private MeshObject mMesh;
+  private DistortedEffects mEffects;
+  private DistortedEffectsPostprocess mPostprocess;
+  private DistortedInputSurface mSurface;
+  private DistortedRenderState mState;
+  private NodeData mData;
+  private int mFboW, mFboH, mFboDepthStencil;
+
+  private class NodeData
+    {
+    long ID;
+    int numPointingNodes;
+    long currTime;
+    ArrayList<Long> key;
+    DistortedFramebuffer mFBO;
+
+    NodeData(long id, ArrayList<Long> k)
+      {
+      ID              = id;
+      key             = k;
+      numPointingNodes= 1;
+      currTime        =-1;
+      mFBO            = null;
+      }
+    }
+ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static synchronized void onPause()
+    {
+    NodeData data;
+
+    for (HashMap.Entry<ArrayList<Long>,NodeData> entry : mMapNodeID.entrySet())
+      {
+      data = entry.getValue();
+
+      if( data.mFBO != null )
+        {
+        data.mFBO.markForDeletion();
+        data.mFBO = null;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static synchronized void onDestroy()
+    {
+    mNextNodeID = 0;
+    mMapNodeID.clear();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private ArrayList<Long> generateIDList()
+    {
+    ArrayList<Long> ret = new ArrayList<>();
+
+    if( mNumChildren[0]==0 )
+      {
+      // add a negative number so this leaf never gets confused with a internal node
+      // with a single child that happens to have ID identical to some leaf's Effects ID.
+      ret.add(-mEffects.getID());
+      }
+    else
+      {
+      DistortedNode node;
+   
+      for(int i=0; i<mNumChildren[0]; i++)
+        {
+        node = mChildren.get(i);
+        ret.add(node.mData.ID);
+        }
+
+      // A bit questionable decision here - we are sorting the children IDs, which means
+      // that order in which we draw the children is going to be undefined (well, this is not
+      // strictly speaking true - when rendering, if no postprocessing and isomorphism are
+      // involved, we *DO* render the children in order they were added; if however there
+      // are two internal nodes with the same list of identical children, just added in a
+      // different order each time, then we consider them isomorphic, i.e. identical and only
+      // render the first one. If then two children of such 'pseudo-isomorphic' nodes are at
+      // exactly the same Z-height this might result in some unexpected sights).
+      //
+      // Reason: with the children being sorted by postprocessing buckets, the order is
+      // undefined anyway (although only when postprocessing is applied).
+      //
+      // See the consequences in the 'Olympic' app - remove a few leaves and add them back in
+      // different order. You will see the number of renders go back to the original 14.
+      Collections.sort(ret);
+      }
+
+    ret.add( 0, mSurface.getID() );
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Debug - print all the Node IDs
+
+  @SuppressWarnings("unused")
+  void debug(int depth)
+    {
+    String tmp="";
+    int i;
+
+    for(i=0; i<depth; i++) tmp +="   ";
+    tmp += ("NodeID="+mData.ID+" nodes pointing: "+mData.numPointingNodes+" surfaceID="+
+            mSurface.getID()+" FBO="+(mData.mFBO==null ? "null":mData.mFBO.getID()))+
+            " parent sID="+(mParent==null ? "null": (mParent.mSurface.getID()));
+
+    android.util.Log.e("NODE", tmp);
+
+    for(i=0; i<mNumChildren[0]; i++)
+      mChildren.get(i).debug(depth+1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Debug - print contents of the HashMap
+
+  @SuppressWarnings("unused")
+  static void debugMap()
+    {
+    NodeData tmp;
+
+    for(ArrayList<Long> key: mMapNodeID.keySet())
+      {
+      tmp = mMapNodeID.get(key);
+      android.util.Log.e("NODE", "NodeID: "+tmp.ID+" <-- "+key);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// tree isomorphism algorithm
+
+  private void adjustIsomorphism()
+    {
+    ArrayList<Long> newList = generateIDList();
+    NodeData newData = mMapNodeID.get(newList);
+
+    if( newData!=null )
+      {
+      newData.numPointingNodes++;
+      }
+    else
+      {
+      newData = new NodeData(++mNextNodeID,newList);
+      mMapNodeID.put(newList,newData);
+      }
+
+    boolean deleteOldFBO = false;
+    boolean createNewFBO = false;
+
+    if( --mData.numPointingNodes==0 )
+      {
+      mMapNodeID.remove(mData.key);
+      if( mData.mFBO!=null ) deleteOldFBO=true;
+      }
+    if( mNumChildren[0]>0 && newData.mFBO==null )
+      {
+      createNewFBO = true;
+      }
+    if( mNumChildren[0]==0 && newData.mFBO!=null )
+      {
+      newData.mFBO.markForDeletion();
+      android.util.Log.d("NODE", "ERROR!! this NodeData cannot possibly contain a non-null FBO!! "+newData.mFBO.getID() );
+      newData.mFBO = null;
+      }
+
+    if( deleteOldFBO && createNewFBO )
+      {
+      newData.mFBO = mData.mFBO;  // just copy over
+      //android.util.Log.d("NODE", "copying over FBOs "+mData.mFBO.getID() );
+      }
+    else if( deleteOldFBO )
+      {
+      mData.mFBO.markForDeletion();
+      //android.util.Log.d("NODE", "deleting old FBO "+mData.mFBO.getID() );
+      mData.mFBO = null;
+      }
+    else if( createNewFBO )
+      {
+      int width  = mFboW <= 0 ? mSurface.getWidth()  : mFboW;
+      int height = mFboH <= 0 ? mSurface.getHeight() : mFboH;
+      newData.mFBO = new DistortedFramebuffer(1,mFboDepthStencil, DistortedSurface.TYPE_TREE, width, height);
+      //android.util.Log.d("NODE", "creating new FBO "+newData.mFBO.getID() );
+      }
+
+    mData = newData;
+
+    if( mParent!=null ) mParent.adjustIsomorphism();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int markStencilAndDepth(long currTime, DistortedOutputSurface surface, DistortedEffectsPostprocess effects)
+    {
+    DistortedInputSurface input = mNumChildren[0]==0 ? mSurface : mData.mFBO;
+
+    if( input.setAsInput() )
+      {
+      surface.setAsOutput();
+      DistortedRenderState.setUpStencilMark();
+      mEffects.drawPriv(mSurface.getWidth() /2.0f, mSurface.getHeight()/2.0f, mMesh, surface, currTime, effects.getHalo()*surface.mMipmap);
+      DistortedRenderState.unsetUpStencilMark();
+
+      return 1;
+      }
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return the total number of render calls issued
+
+  int draw(long currTime, DistortedOutputSurface surface)
+    {
+    DistortedInputSurface input = mNumChildren[0]==0 ? mSurface : mData.mFBO;
+
+    if( input.setAsInput() )
+      {
+      surface.setAsOutput(currTime);
+      mState.apply();
+      mEffects.drawPriv(mSurface.getWidth()/2.0f, mSurface.getHeight()/2.0f, mMesh, surface, currTime, 0);
+      return 1;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return the total number of render calls issued
+
+  int renderRecursive(long currTime)
+    {
+    int numRenders = 0;
+
+    if( mNumChildren[0]>0 && mData.currTime!=currTime )
+      {
+      mData.currTime = currTime;
+
+      for (int i=0; i<mNumChildren[0]; i++)
+        {
+        numRenders += mChildren.get(i).renderRecursive(currTime);
+        }
+
+      if( mData.mFBO==null )
+        {
+        int width  = mFboW <= 0 ? mSurface.getWidth()  : mFboW;
+        int height = mFboH <= 0 ? mSurface.getHeight() : mFboH;
+        mData.mFBO = new DistortedFramebuffer(1,mFboDepthStencil, DistortedSurface.TYPE_TREE, width, height);
+        }
+
+      mData.mFBO.setAsOutput(currTime);
+
+      if( mSurface.setAsInput() )
+        {
+        numRenders++;
+        DistortedEffects.blitPriv(mData.mFBO);
+        }
+
+      numRenders += mData.mFBO.renderChildren(currTime,mNumChildren[0],mChildren);
+      }
+
+    return numRenders;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void newJob(int t, DistortedNode n, DistortedEffectsPostprocess d)
+    {
+    mJobs.add(new Job(t,n,d));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setPost(DistortedEffectsPostprocess dep)
+    {
+    mPostprocess = dep;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setSurfaceParent(DistortedOutputSurface dep)
+    {
+    mSurfaceParent = dep;
+    mParent = null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Constructs new Node.
+ *     
+ * @param surface InputSurface to put into the new Node.
+ * @param effects DistortedEffects to put into the new Node.
+ * @param mesh MeshObject to put into the new Node.
+ */
+  public DistortedNode(DistortedInputSurface surface, DistortedEffects effects, MeshObject mesh)
+    {
+    mSurface       = surface;
+    mEffects       = effects;
+    mPostprocess   = null;
+    mMesh          = mesh;
+    mState         = new DistortedRenderState();
+    mChildren      = null;
+    mNumChildren   = new int[1];
+    mNumChildren[0]= 0;
+    mParent        = null;
+    mSurfaceParent = null;
+
+    mFboW            = 0;  // i.e. take this from
+    mFboH            = 0;  // mSurface's dimensions
+    mFboDepthStencil = DistortedFramebuffer.DEPTH_NO_STENCIL;
+
+    ArrayList<Long> list = new ArrayList<>();
+    list.add(mSurface.getID());
+    list.add(-mEffects.getID());
+
+    mData = mMapNodeID.get(list);
+   
+    if( mData!=null )
+      {
+      mData.numPointingNodes++;
+      }
+    else
+      {
+      mData = new NodeData(++mNextNodeID,list);
+      mMapNodeID.put(list, mData);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////  
+/**
+ * Copy-constructs new Node from another Node.
+ *     
+ * @param node The DistortedNode to copy data from.
+ * @param flags bit field composed of a subset of the following:
+ *        {@link Distorted#CLONE_SURFACE},  {@link Distorted#CLONE_MATRIX}, {@link Distorted#CLONE_VERTEX},
+ *        {@link Distorted#CLONE_FRAGMENT} and {@link Distorted#CLONE_CHILDREN}.
+ *        For example flags = CLONE_SURFACE | CLONE_CHILDREN.
+ */
+  public DistortedNode(DistortedNode node, int flags)
+    {
+    mEffects      = new DistortedEffects(node.mEffects,flags);
+    mPostprocess  = null;
+    mMesh         = node.mMesh;
+    mState        = new DistortedRenderState();
+    mParent       = null;
+    mSurfaceParent= null;
+
+    mFboW            = node.mFboW;
+    mFboH            = node.mFboH;
+    mFboDepthStencil = node.mFboDepthStencil;
+
+    if( (flags & Distorted.CLONE_SURFACE) != 0 )
+      {
+      mSurface = node.mSurface;
+      }
+    else
+      {
+      int w = node.mSurface.getWidth();
+      int h = node.mSurface.getHeight();
+
+      if( node.mSurface instanceof DistortedTexture )
+        {
+        mSurface = new DistortedTexture(w,h, DistortedSurface.TYPE_TREE);
+        }
+      else if( node.mSurface instanceof DistortedFramebuffer )
+        {
+        int depthStencil = DistortedFramebuffer.NO_DEPTH_NO_STENCIL;
+
+        if( ((DistortedFramebuffer) node.mSurface).hasDepth() )
+          {
+          boolean hasStencil = ((DistortedFramebuffer) node.mSurface).hasStencil();
+          depthStencil = (hasStencil ? DistortedFramebuffer.BOTH_DEPTH_STENCIL:DistortedFramebuffer.DEPTH_NO_STENCIL);
+          }
+
+        mSurface = new DistortedFramebuffer(1,depthStencil,DistortedSurface.TYPE_TREE,w,h);
+        }
+      }
+    if( (flags & Distorted.CLONE_CHILDREN) != 0 )
+      {
+      if( node.mChildren==null )     // do NOT copy over the NULL!
+        {
+        node.mChildren = new ArrayList<>(2);
+        }
+
+      mChildren = node.mChildren;
+      mNumChildren = node.mNumChildren;
+      }
+    else
+      {
+      mChildren = null;
+      mNumChildren = new int[1];
+      mNumChildren[0] = 0;
+      }
+   
+    ArrayList<Long> list = generateIDList();
+   
+    mData = mMapNodeID.get(list);
+   
+    if( mData!=null )
+      {
+      mData.numPointingNodes++;
+      }
+    else
+      {
+      mData = new NodeData(++mNextNodeID,list);
+      mMapNodeID.put(list, mData);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Adds a new child to the last position in the list of our Node's children.
+ * <p>
+ * We cannot do this mid-render - actual attachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param node The new Node to add.
+ */
+  public void attach(DistortedNode node)
+    {
+    mJobs.add(new Job(ATTACH,node,null));
+    DistortedMaster.newSlave(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Adds a new child to the last position in the list of our Node's children.
+ * <p>
+ * We cannot do this mid-render - actual attachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param surface InputSurface to initialize our child Node with.
+ * @param effects DistortedEffects to initialize our child Node with.
+ * @param mesh MeshObject to initialize our child Node with.
+ * @return the newly constructed child Node, or null if we couldn't allocate resources.
+ */
+  public DistortedNode attach(DistortedInputSurface surface, DistortedEffects effects, MeshObject mesh)
+    {
+    DistortedNode node = new DistortedNode(surface,effects,mesh);
+    mJobs.add(new Job(ATTACH,node,null));
+    DistortedMaster.newSlave(this);
+    return node;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes the first occurrence of a specified child from the list of children of our Node.
+ * <p>
+ * We cannot do this mid-render - actual detachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param node The Node to remove.
+ */
+  public void detach(DistortedNode node)
+    {
+    mJobs.add(new Job(DETACH,node,null));
+    DistortedMaster.newSlave(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes the first occurrence of a specified child from the list of children of our Node.
+ * <p>
+ * A bit questionable method as there can be many different Nodes attached as children, some
+ * of them having the same Effects but - for instance - different Mesh. Use with care.
+ * <p>
+ * We cannot do this mid-render - actual detachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param effects DistortedEffects to remove.
+ */
+  public void detach(DistortedEffects effects)
+    {
+    long id = effects.getID();
+    DistortedNode node;
+    boolean detached = false;
+
+    for(int i=0; i<mNumChildren[0]; i++)
+      {
+      node = mChildren.get(i);
+
+      if( node.getEffects().getID()==id )
+        {
+        detached = true;
+        mJobs.add(new Job(DETACH,node,null));
+        DistortedMaster.newSlave(this);
+        break;
+        }
+      }
+
+    if( !detached )
+      {
+      // if we failed to detach any, it still might be the case that
+      // there's an ATTACH job that we need to cancel.
+      int num = mJobs.size();
+      Job job;
+
+      for(int i=0; i<num; i++)
+        {
+        job = mJobs.get(i);
+
+        if( job.type==ATTACH && job.node.getEffects()==effects )
+          {
+          mJobs.remove(i);
+          break;
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes all children Nodes.
+ * <p>
+ * We cannot do this mid-render - actual detachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ */
+  public void detachAll()
+    {
+    mJobs.add(new Job(DETALL,null,null));
+    DistortedMaster.newSlave(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * This is not really part of the public API. Has to be public only because it is a part of the
+ * DistortedSlave interface, which should really be a class that we extend here instead but
+ * Java has no multiple inheritance.
+ *
+ * @y.exclude
+ */
+  public void doWork()
+    {
+    int num = mJobs.size();
+    Job job;
+
+    int numChanges=0;
+
+    for(int i=0; i<num; i++)
+      {
+      job = mJobs.remove(0);
+
+      switch(job.type)
+        {
+        case ATTACH: numChanges++;
+                     if( mChildren==null ) mChildren = new ArrayList<>(2);
+                     job.node.mParent = this;
+                     job.node.mSurfaceParent = null;
+                     DistortedMaster.addSorted(mChildren,job.node);
+                     mNumChildren[0]++;
+                     break;
+        case DETACH: numChanges++;
+                     if( mNumChildren[0]>0 && mChildren.remove(job.node) )
+                       {
+                       job.node.mParent = null;
+                       job.node.mSurfaceParent = null;
+                       mNumChildren[0]--;
+                       }
+                     break;
+        case DETALL: numChanges++;
+                     if( mNumChildren[0]>0 )
+                       {
+                       DistortedNode tmp;
+
+                       for(int j=mNumChildren[0]-1; j>=0; j--)
+                         {
+                         tmp = mChildren.remove(j);
+                         tmp.mParent = null;
+                         tmp.mSurfaceParent = null;
+                         }
+
+                       mNumChildren[0] = 0;
+                       }
+                     break;
+        case SORT  : job.node.mPostprocess = job.dep;
+                     mChildren.remove(job.node);
+                     DistortedMaster.addSorted(mChildren,job.node);
+                     break;
+        }
+      }
+
+    if( numChanges>0 ) adjustIsomorphism();
+    }
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Sets the Postprocessing Effects we will apply to the temporary buffer this Node - and fellow siblings
+ * with the same Effects - will get rendered to.
+ * <p>
+ * For efficiency reasons, it is very important to assign the very same DistortedEffectsPostprocess
+ * object to all the DistortedNode siblings that are supposed to be postprocessed in the same way,
+ * because only then will the library assign all such siblings to the same 'Bucket' which gets rendered
+ * to the same offscreen buffer which then gets postprocessed in one go and subsequently merged to the
+ * target Surface.
+ */
+  public void setPostprocessEffects(DistortedEffectsPostprocess dep)
+    {
+    if( mParent!=null )
+      {
+      mParent.newJob(SORT, this, dep);
+      DistortedMaster.newSlave(mParent);
+      }
+    else if( mSurfaceParent!=null )
+      {
+      mSurfaceParent.newJob(SORT, this, dep);
+      DistortedMaster.newSlave(mSurfaceParent);
+      }
+    else
+      {
+      mPostprocess = dep;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns the DistortedEffectsPostprocess object that's in the Node.
+ *
+ * @return The DistortedEffectsPostprocess contained in the Node.
+ */
+  public DistortedEffectsPostprocess getEffectsPostprocess()
+    {
+    return mPostprocess;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns the DistortedEffects object that's in the Node.
+ * 
+ * @return The DistortedEffects contained in the Node.
+ */
+  public DistortedEffects getEffects()
+    {
+    return mEffects;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Returns the DistortedInputSurface object that's in the Node.
+ *
+ * @return The DistortedInputSurface contained in the Node.
+ */
+  public DistortedInputSurface getSurface()
+    {
+    return mSurface;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Resizes the DistortedFramebuffer object that we render this Node to.
+ */
+  public void resize(int width, int height)
+    {
+    mFboW = width;
+    mFboH = height;
+
+    if ( mData.mFBO !=null )
+      {
+      // TODO: potentially allocate a new NodeData if we have to
+      mData.mFBO.resize(width,height);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Enables/disables DEPTH and STENCIL buffers in the Framebuffer object that we render this Node to.
+ */
+  public void enableDepthStencil(int depthStencil)
+    {
+    mFboDepthStencil = depthStencil;
+
+    if ( mData.mFBO !=null )
+      {
+      // TODO: potentially allocate a new NodeData if we have to
+      mData.mFBO.enableDepthStencil(depthStencil);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// APIs that control how to set the OpenGL state just before rendering this Node.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, use ColorMask (r,g,b,a).
+ *
+ * @param r Write to the RED color channel when rendering this Node?
+ * @param g Write to the GREEN color channel when rendering this Node?
+ * @param b Write to the BLUE color channel when rendering this Node?
+ * @param a Write to the ALPHA channel when rendering this Node?
+ */
+  @SuppressWarnings("unused")
+  public void glColorMask(boolean r, boolean g, boolean b, boolean a)
+    {
+    mState.glColorMask(r,g,b,a);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, switch on writing to Depth buffer?
+ *
+ * @param mask Write to the Depth buffer when rendering this Node?
+ */
+  @SuppressWarnings("unused")
+  public void glDepthMask(boolean mask)
+    {
+    mState.glDepthMask(mask);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, which bits of the Stencil buffer to write to?
+ *
+ * @param mask Marks the bits of the Stencil buffer we will write to when rendering this Node.
+ */
+  @SuppressWarnings("unused")
+  public void glStencilMask(int mask)
+    {
+    mState.glStencilMask(mask);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, which Tests to enable?
+ *
+ * @param test Valid values: GL_DEPTH_TEST, GL_STENCIL_TEST, GL_BLEND
+ */
+  @SuppressWarnings("unused")
+  public void glEnable(int test)
+    {
+    mState.glEnable(test);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, which Tests to enable?
+ *
+ * @param test Valid values: GL_DEPTH_TEST, GL_STENCIL_TEST, GL_BLEND
+ */
+  @SuppressWarnings("unused")
+  public void glDisable(int test)
+    {
+    mState.glDisable(test);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, use the following StencilFunc.
+ *
+ * @param func Valid values: GL_NEVER, GL_ALWAYS, GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATER, GL_NOTEQUAL
+ * @param ref  Reference valut to compare our stencil with.
+ * @param mask Mask used when comparing.
+ */
+  @SuppressWarnings("unused")
+  public void glStencilFunc(int func, int ref, int mask)
+    {
+    mState.glStencilFunc(func,ref,mask);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, use the following StencilOp.
+ * <p>
+ * Valid values of all 3 parameters: GL_KEEP, GL_ZERO, GL_REPLACE, GL_INCR, GL_DECR, GL_INVERT, GL_INCR_WRAP, GL_DECR_WRAP
+ *
+ * @param sfail  What to do when Stencil Test fails.
+ * @param dpfail What to do when Depth Test fails.
+ * @param dppass What to do when Depth Test passes.
+ */
+  @SuppressWarnings("unused")
+  public void glStencilOp(int sfail, int dpfail, int dppass)
+    {
+    mState.glStencilOp(sfail,dpfail,dppass);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, use the following DepthFunc.
+ *
+ * @param func Valid values: GL_NEVER, GL_ALWAYS, GL_LESS, GL_LEQUAL, GL_EQUAL, GL_GEQUAL, GL_GREATER, GL_NOTEQUAL
+ */
+  @SuppressWarnings("unused")
+  public void glDepthFunc(int func)
+    {
+    mState.glDepthFunc(func);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Node, use the following Blending mode.
+ * <p>
+ * Valid values: GL_ZERO, GL_ONE, GL_SRC_COLOR, GL_ONE_MINUS_SRC_COLOR, GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA,
+ *               GL_DST_ALPHA, GL_ONE_MINUS_DST_ALPHA, GL_CONSTANT_COLOR, GL_ONE_MINUS_CONSTANT_COLOR,
+ *               GL_CONSTANT_ALPHA, GL_ONE_MINUS_CONSTANT_ALPHA, GL_SRC_ALPHA_SATURATE
+ *
+ * @param src Source Blend function
+ * @param dst Destination Blend function
+ */
+  @SuppressWarnings("unused")
+  public void glBlendFunc(int src, int dst)
+    {
+    mState.glBlendFunc(src,dst);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Before rendering this Node, clear the following buffers.
+ * <p>
+ * Valid values: 0, or bitwise OR of one or more values from the set GL_COLOR_BUFFER_BIT,
+ *               GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT.
+ * Default: 0
+ *
+ * @param mask bitwise OR of BUFFER_BITs to clear.
+ */
+  @SuppressWarnings("unused")
+  public void glClear(int mask)
+    {
+    mState.glClear(mask);
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/DistortedObject.java b/src/main/java/org/distorted/library/main/DistortedObject.java
new file mode 100644
index 0000000..c18b5f5
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedObject.java
@@ -0,0 +1,233 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import java.util.HashMap;
+import java.util.LinkedList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Any Object which gets uploaded to GPU memory and thus needs to be re-created (transparently to
+ * applications!) whenever we lose OpenGL context.
+ *
+ * Keep all objects created in a static LinkedList. The point: we need to be able to mark
+ * Objects for deletion, and delete all marked Objects later at a convenient time (that's
+ * because we can only delete from a thread that holds the OpenGL context so here we provide a
+ * framework where one is able to mark for deletion at any time and actual deletion takes place
+ * on the next render).
+*/
+abstract class DistortedObject
+{
+  static final int FAILED_TO_CREATE = 1;
+  static final int NOT_CREATED_YET  = 2;
+  static final int DONT_CREATE      = 3;
+  static final int CREATED          = 4;
+
+  static final int TYPE_USER = 0;
+  static final int TYPE_TREE = 1;
+  static final int TYPE_SYST = 2;
+
+  private static final int JOB_CREATE = 0;
+  private static final int JOB_DELETE = 1;
+
+  private class Job
+    {
+    DistortedObject object;
+    int action;
+
+    Job(DistortedObject o, int a)
+      {
+      object = o;
+      action = a;
+      }
+    }
+
+  private static boolean mToDo = false;
+  private static LinkedList<DistortedObject> mDoneList = new LinkedList<>();
+  private static HashMap<Long,Job> mToDoMap = new HashMap<>();
+  private static long mNextClientID = 0;
+  private static long mNextSystemID = 0;
+
+  private long mID;
+  private int mType;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  abstract void create();
+  abstract void delete();
+  abstract void recreate();
+  abstract String printDetails();
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// must be called from a thread holding OpenGL Context
+
+  static synchronized boolean toDo()
+    {
+    if( mToDo )
+      {
+      Job job;
+      DistortedObject object;
+
+      for(Long key: mToDoMap.keySet())
+        {
+        job = mToDoMap.get(key);
+        object = job.object;
+
+        //android.util.Log.d("Object", object.getClass().getSimpleName()+"  ---> need to "
+        //                  +(job.action==JOB_CREATE ? "create":"delete")+" objectID="+object.mID );
+
+        if( job.action==JOB_CREATE )
+          {
+          object.create();
+          mDoneList.add(object);
+          }
+        else if( job.action==JOB_DELETE )
+          {
+          object.delete();
+          }
+        }
+
+      mToDoMap.clear();
+      mToDo = false;
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static synchronized void onPause()
+    {
+    DistortedObject object;
+    int num = mDoneList.size();
+
+    for(int i=0; i<num; i++)
+      {
+      object = mDoneList.removeFirst();
+      mToDoMap.put(object.mID, object.new Job(object,JOB_CREATE) );
+      object.recreate();
+      }
+
+    mToDo = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static synchronized void onDestroy()
+    {
+    mToDoMap.clear();
+    mDoneList.clear();
+
+    mToDo = true;
+    mNextClientID = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  static void debugLists()
+    {
+    android.util.Log.e("Object", "Done list:");
+
+    DistortedObject object;
+    int num = mDoneList.size();
+
+    for(int i=0; i<num; i++)
+      {
+      object = mDoneList.get(i);
+      object.print("");
+      }
+
+    android.util.Log.e("Object", "ToDo list:");
+
+    Job job;
+
+    for(Long key: mToDoMap.keySet())
+      {
+      job = mToDoMap.get(key);
+      job.object.print(job.action==JOB_CREATE ? " create":" delete");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void print(String msg)
+    {
+    String str = "ID:"+mID;
+
+    switch(mType)
+      {
+      case TYPE_SYST: str+=" SYSTEM "; break;
+      case TYPE_USER: str+=" USER   "; break;
+      case TYPE_TREE: str+=" TREE   "; break;
+      default       : str+=" ERROR? ";
+      }
+
+    android.util.Log.e("Object", str+printDetails()+msg);
+    }
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedObject(int create, int type)
+    {
+    mID  = type==TYPE_SYST ? --mNextSystemID : ++mNextClientID;
+    mType= type;
+
+    if( create!=DONT_CREATE )
+      {
+      mToDoMap.put(mID, new Job(this,JOB_CREATE) );
+      mToDo = true;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void markForCreation()
+    {
+    mDoneList.remove(this);
+    mToDoMap.put(mID, new Job(this,JOB_CREATE) );
+    mToDo = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Mark the underlying OpenGL object for deletion. Actual deletion will take place on the next render.
+ */
+  synchronized public void markForDeletion()
+    {
+    mDoneList.remove(this);
+    mToDoMap.put(mID, new Job(this,JOB_DELETE) );
+    mToDo = true;
+    }
+
+////////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Return unique ID of this Object.
+ */
+  public long getID()
+    {
+    return mID;
+    }
+
+}
diff --git a/src/main/java/org/distorted/library/main/DistortedOutputSurface.java b/src/main/java/org/distorted/library/main/DistortedOutputSurface.java
new file mode 100644
index 0000000..2cb4105
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedOutputSurface.java
@@ -0,0 +1,764 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.opengl.GLES30;
+import android.opengl.Matrix;
+
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+abstract class DistortedOutputSurface extends DistortedSurface implements DistortedSlave
+{
+//////////// DEBUG FLAGS /////////////////////////////////////////////
+/**
+ * When rendering a Screen, show FPS in the upper-left corner?
+ */
+public static final int DEBUG_FPS = 1;
+//////////// END DEBUG FLAGS /////////////////////////////////////////
+
+/**
+ * Do not create DEPTH or STENCIL attachment
+ */
+  public static final int NO_DEPTH_NO_STENCIL = 0;
+/**
+ * Create DEPTH, but not STENCIL
+ */
+  public static final int DEPTH_NO_STENCIL    = 1;
+/**
+ * Create both DEPTH and STENCIL
+ */
+  public static final int BOTH_DEPTH_STENCIL  = 2;
+
+  private static final int ATTACH = 0;
+  private static final int DETACH = 1;
+  private static final int DETALL = 2;
+  private static final int SORT   = 3;
+
+  private ArrayList<DistortedNode> mChildren;
+  private int mNumChildren;   // ==mChildren.length(), but we only create mChildren if the first one gets added
+
+  private class Job
+    {
+    int type;
+    DistortedNode node;
+    DistortedEffectsPostprocess dep;
+
+    Job(int t, DistortedNode n, DistortedEffectsPostprocess d)
+      {
+      type = t;
+      node = n;
+      dep  = d;
+      }
+    }
+
+  private ArrayList<Job> mJobs = new ArrayList<>();
+
+  DistortedFramebuffer[] mBuffer;
+
+  private long mTime;
+  private float mFOV;
+  float mDistance, mNear;
+  float[] mProjectionMatrix;
+
+  int mDepthStencilCreated;
+  int mDepthStencil;
+  int[] mDepthStencilH = new int[1];
+  int[] mFBOH          = new int[1];
+
+  private float mClearR, mClearG, mClearB, mClearA;
+  private float mClearDepth;
+  private int mClearStencil;
+  private int mClear;
+  float mMipmap;
+
+  private int mDebugLevel;
+
+private String sLast="", sCurr="";
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  abstract void prepareDebug(long time);
+  abstract void renderDebug(long time);
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedOutputSurface(int width, int height, int createColor, int numcolors, int depthStencil, int fbo, int type)
+    {
+    super(width,height,createColor,numcolors,type);
+
+    mProjectionMatrix = new float[16];
+
+    mFOV = 60.0f;
+    mNear=  0.5f;
+
+    mDepthStencilCreated= (depthStencil== NO_DEPTH_NO_STENCIL ? DONT_CREATE:NOT_CREATED_YET);
+    mDepthStencil = depthStencil;
+
+    mFBOH[0]         = fbo;
+    mDepthStencilH[0]= 0;
+
+    mTime = 0;
+    mDebugLevel = 0;
+
+    mClearR = 0.0f;
+    mClearG = 0.0f;
+    mClearB = 0.0f;
+    mClearA = 0.0f;
+
+    mClearDepth = 1.0f;
+    mClearStencil = 0;
+    mClear = GLES30.GL_DEPTH_BUFFER_BIT | GLES30.GL_COLOR_BUFFER_BIT;
+
+    mBuffer = new DistortedFramebuffer[EffectQuality.LENGTH];
+
+    mMipmap = 1.0f;
+
+    createProjection();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createProjection()
+    {
+    if( mWidth>0 && mHeight>1 )
+      {
+      if( mFOV>0.0f )  // perspective projection
+        {
+        float a = 2.0f*(float)Math.tan(mFOV*Math.PI/360);
+        float q = mWidth*mNear;
+        float c = mHeight*mNear;
+
+        float left   = -q/2;
+        float right  = +q/2;
+        float bottom = -c/2;
+        float top    = +c/2;
+        float near   =  c/a;
+
+        mDistance    = mHeight/a;
+        float far    = 2*mDistance-near;
+
+        Matrix.frustumM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
+        }
+      else             // parallel projection
+        {
+        float left   = -mWidth/2.0f;
+        float right  = +mWidth/2.0f;
+        float bottom = -mHeight/2.0f;
+        float top    = +mHeight/2.0f;
+        float near   = mWidth+mHeight-mHeight*(1.0f-mNear);
+        mDistance    = mWidth+mHeight;
+        float far    = mWidth+mHeight+mHeight*(1.0f-mNear);
+
+        Matrix.orthoM(mProjectionMatrix, 0, left, right, bottom, top, near, far);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void createBuffers()
+    {
+    float mipmap=1.0f;
+
+    for(int j=0; j<EffectQuality.LENGTH; j++)
+      {
+      mBuffer[j] = new DistortedFramebuffer(2,BOTH_DEPTH_STENCIL,TYPE_SYST, (int)(mWidth*mipmap), (int)(mHeight*mipmap) );
+      mBuffer[j].mMipmap = mipmap;
+      mipmap *= EffectQuality.MULTIPLIER;
+      }
+
+    DistortedObject.toDo(); // create the FBOs immediately. This is safe as we must be holding the OpenGL context now.
+
+    GLES30.glStencilMask(0xff);
+    GLES30.glDepthMask(true);
+    GLES30.glColorMask(true,true,true,true);
+    GLES30.glClearColor(0.0f,0.0f,0.0f,0.0f);
+    GLES30.glClearDepthf(1.0f);
+    GLES30.glClearStencil(0);
+
+    for(int j=0; j<EffectQuality.LENGTH; j++)
+      {
+      mBuffer[j].setAsOutput();
+      GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mBuffer[j].mColorH[1], 0);
+      GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT|GLES30.GL_DEPTH_BUFFER_BIT|GLES30.GL_STENCIL_BUFFER_BIT);
+      GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, mBuffer[j].mColorH[0], 0);
+      GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int blitWithDepth(int quality, long currTime)
+    {
+    DistortedFramebuffer buffer = mBuffer[quality];
+
+    GLES30.glViewport(0, 0, mWidth, mHeight);
+    setAsOutput(currTime);
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mColorH[0]);
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE1);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mDepthStencilH[0]);
+
+    GLES30.glDisable(GLES30.GL_STENCIL_TEST);
+    GLES30.glStencilMask(0x00);
+
+    DistortedEffects.blitDepthPriv(this);
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE1);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
+
+    // clear buffers
+    GLES30.glStencilMask(0xff);
+    GLES30.glDepthMask(true);
+    GLES30.glColorMask(true,true,true,true);
+    GLES30.glClearColor(0.0f,0.0f,0.0f,0.0f);
+    GLES30.glClearDepthf(1.0f);
+    GLES30.glClearStencil(0);
+
+    buffer.setAsOutput();
+    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[1], 0);
+    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT|GLES30.GL_DEPTH_BUFFER_BIT|GLES30.GL_STENCIL_BUFFER_BIT);
+    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[0], 0);
+    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
+
+    return 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Render all children, one by one. If there are no postprocessing effects, just render to THIS.
+// Otherwise, render to a buffer and on each change of Postprocessing Bucket, apply the postprocessing
+// to a whole buffer and merge it.
+
+  int renderChildren(long time, int num, ArrayList<DistortedNode> children)
+    {
+    int numRenders = 0;
+    DistortedNode child1, child2;
+    DistortedEffectsPostprocess lastP=null, currP;
+    long lastB=0, currB;
+    int bucketChange=0;
+    int lastQ=0, currQ;
+
+sCurr = "";
+
+    for(int i=0; i<num; i++)
+      {
+      child1 = children.get(i);
+      currP = child1.getEffectsPostprocess();
+      currB = currP==null ? 0 : currP.getBucket();
+      currQ = currP==null ? 0 : currP.getQuality();
+
+sCurr += (" "+currB);
+
+      if( currB==0 ) numRenders += child1.draw(time,this);
+      else
+        {
+        if( mBuffer[0]==null ) createBuffers();
+
+        if( lastB!=currB )
+          {
+          if( lastB!=0 )
+            {
+            for(int j=bucketChange; j<i; j++)
+              {
+              child2 = children.get(j);
+              numRenders += child2.markStencilAndDepth(time,mBuffer[lastQ],lastP);
+              }
+
+            numRenders += lastP.postprocess(time, this);
+            numRenders += blitWithDepth(lastQ,time);
+            }
+
+          bucketChange = i;
+          }
+
+        numRenders += child1.draw(time,mBuffer[currQ]);
+
+        if( i==num-1 )
+          {
+          for(int j=bucketChange; j<num; j++)
+            {
+            child2 = children.get(j);
+            numRenders += child2.markStencilAndDepth(time,mBuffer[currQ],currP);
+            }
+
+          numRenders += currP.postprocess(time,this);
+          numRenders += blitWithDepth(currQ,time);
+          }
+        }
+
+      lastQ = currQ;
+      lastP = currP;
+      lastB = currB;
+      }
+
+if( !sLast.equals(sCurr) ) android.util.Log.e("surface", "rendering: "+sCurr);
+sLast = sCurr;
+
+    return numRenders;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void newJob(int t, DistortedNode n, DistortedEffectsPostprocess d)
+    {
+    mJobs.add(new Job(t,n,d));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setAsOutput()
+    {
+    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Make the library show various debugging information.
+ * <p>
+ * Currently only DEBUG_FPS - show FPS in the upper-left corner of every Screen - is defined.
+ *
+ * @param bitmask 0, or a bitmask of DEBUG_** flags to enable (currently only DEBUG_FPS defined)
+ */
+  public void setDebug(int bitmask)
+    {
+    mDebugLevel = bitmask;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Draws all the attached children to this OutputSurface.
+ * <p>
+ * Must be called from a thread holding OpenGL Context.
+ *
+ * @param time Current time, in milliseconds. This will be passed to all the Effects stored in the children Nodes.
+ * @return Number of objects rendered.
+ */
+  public int render(long time)
+    {
+    boolean showDebug = ( mDebugLevel!=0 && this instanceof DistortedScreen );
+
+    if( showDebug ) prepareDebug(time);
+
+    // change tree topology (attach and detach children)
+/*
+    boolean changed1 =
+*/
+    DistortedMaster.toDo();
+/*
+    if( changed1 )
+      {
+      for(int i=0; i<mNumChildren; i++)
+        {
+        mChildren.get(i).debug(0);
+        }
+
+      DistortedNode.debugMap();
+      }
+*/
+    // create and delete all underlying OpenGL resources
+    // Watch out: FIRST change topology, only then deal
+    // with OpenGL resources. That's because changing Tree
+    // can result in additional Framebuffers that would need
+    // to be created immediately, before the calls to drawRecursive()
+/*
+    boolean changed2 =
+*/
+    toDo();
+/*
+    if( changed2 )
+      {
+      DistortedObject.debugLists();
+      }
+*/
+    // mark OpenGL state as unknown
+    DistortedRenderState.reset();
+
+    int numRenders=0;
+
+    for(int i=0; i<mNumChildren; i++)
+      {
+      numRenders += mChildren.get(i).renderRecursive(time);
+      }
+
+    setAsOutput(time);
+    numRenders += renderChildren(time,mNumChildren,mChildren);
+
+    if( showDebug ) renderDebug(time);
+
+    return numRenders;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Bind this Surface as a Framebuffer we can render to.
+ *
+ * @param time Present time, in milliseconds. The point: looking at this param the library can figure
+ *             out if this is the first time during present frame that this FBO is being set as output.
+ *             If so, the library, in addition to binding the Surface for output, also clears the
+ *             Surface's color and depth attachments.
+ */
+  public void setAsOutput(long time)
+    {
+    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, mFBOH[0]);
+
+    if( mTime!=time )
+      {
+      mTime = time;
+      DistortedRenderState.colorDepthStencilOn();
+      GLES30.glClearColor(mClearR, mClearG, mClearB, mClearA);
+      GLES30.glClearDepthf(mClearDepth);
+      GLES30.glClearStencil(mClearStencil);
+      GLES30.glClear(mClear);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Set mipmap level.
+ * <p>
+ * Trick for speeding up your renders - one can create a pyramid of OutputSurface objects, each next
+ * one some constant FACTOR smaller than the previous (0.5 is the common value), then set the Mipmap
+ * Level of the i-th object to be FACTOR^i (we start counting from 0). When rendering any scene into
+ * such prepared OutputSurface, the library will make sure to scale any Effects used so that the end
+ * scene will end up looking identical no matter which object we render to. Identical, that is, except
+ * for the loss of quality and gain in speed associated with rendering to a smaller Surface.
+ * <p>
+ * Example: if you create two FBOs, one 1000x1000 and another 500x500 in size, and set the second one
+ * mipmap to 0.5 (the first one's is 1.0 by default), define Effects to be a single move by (100,100),
+ * and render a skinned Mesh into both FBO, the end result will look proportionally the same, because
+ * in the second case the move vector (100,100) will be auto-scaled to (50,50).
+ *
+ * @param mipmap The mipmap level. Acceptable range: 0&lt;mipmap&lt;infinity, although mipmap&gt;1
+ *               does not make any sense (that would result in loss of speed and no gain in quality)
+ */
+  public void setMipmap(float mipmap)
+    {
+    mMipmap = mipmap;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Set the (R,G,B,A) values of GLES30.glClearColor() to set up color with which to clear
+ * this Surface at the beginning of each frame.
+ *
+ * @param r the Red component. Default: 0.0f
+ * @param g the Green component. Default: 0.0f
+ * @param b the Blue component. Default: 0.0f
+ * @param a the Alpha component. Default: 0.0f
+ */
+  public void glClearColor(float r, float g, float b, float a)
+    {
+    mClearR = r;
+    mClearG = g;
+    mClearB = b;
+    mClearA = a;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Uses glClearDepthf() to set up a value with which to clear
+ * the Depth buffer of our Surface at the beginning of each frame.
+ *
+ * @param d the Depth. Default: 1.0f
+ */
+  public void glClearDepthf(float d)
+    {
+    mClearDepth = d;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Uses glClearStencil() to set up a value with which to clear the
+ * Stencil buffer of our Surface at the beginning of each frame.
+ *
+ * @param s the Stencil. Default: 0
+ */
+  public void glClearStencil(int s)
+    {
+    mClearStencil = s;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Which buffers to Clear at the beginning of each frame?
+ * <p>
+ * Valid values: 0, or bitwise OR of one or more values from the set GL_COLOR_BUFFER_BIT,
+ *               GL_DEPTH_BUFFER_BIT, GL_STENCIL_BUFFER_BIT.
+ * Default: GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT.
+ *
+ * @param mask bitwise OR of BUFFER_BITs to clear.
+ */
+  public void glClear(int mask)
+    {
+    mClear = mask;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create new Projection matrix.
+ *
+ * @param fov Vertical 'field of view' of the Projection frustrum (in degrees).
+ *            Valid values: 0<=fov<180. FOV==0 means 'parallel projection'.
+ * @param near Distance between the screen plane and the near plane.
+ *             Valid vaules: 0<near<1. When near==0, the Near Plane is exactly at the tip of the
+ *             pyramid. When near==1 (illegal!) the near plane is equivalent to the screen plane.
+ */
+  public void setProjection(float fov, float near)
+    {
+    if( fov < 180.0f && fov >=0.0f )
+      {
+      mFOV = fov;
+      }
+
+    if( near<   1.0f && near> 0.0f )
+      {
+      mNear= near;
+      }
+    else if( near<=0.0f )
+      {
+      mNear = 0.01f;
+      }
+    else if( near>=1.0f )
+      {
+      mNear=0.99f;
+      }
+
+    createProjection();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Resize the underlying Framebuffer.
+ * <p>
+ * This method can be safely called mid-render as it doesn't interfere with rendering.
+ *
+ * @param width The new width.
+ * @param height The new height.
+ */
+  public void resize(int width, int height)
+    {
+    if( mWidth!=width || mHeight!=height )
+      {
+      mWidth = width;
+      mHeight= height;
+
+      createProjection();
+
+      if( mColorCreated==CREATED )
+        {
+        markForCreation();
+        recreate();
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Return true if the Surface contains a DEPTH attachment.
+ *
+ * @return <bold>true</bold> if the Surface contains a DEPTH attachment.
+ */
+  public boolean hasDepth()
+    {
+    return mDepthStencilCreated==CREATED;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Return true if the Surface contains a STENCIL attachment.
+ *
+ * @return <bold>true</bold> if the Surface contains a STENCIL attachment.
+ */
+  public boolean hasStencil()
+    {
+    return (mDepthStencilCreated==CREATED && mDepthStencil==BOTH_DEPTH_STENCIL);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Adds a new child to the last position in the list of our Surface's children.
+ * <p>
+ * We cannot do this mid-render - actual attachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param node The new Node to add.
+ */
+  public void attach(DistortedNode node)
+    {
+    mJobs.add(new Job(ATTACH,node,null));
+    DistortedMaster.newSlave(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Adds a new child to the last position in the list of our Surface's children.
+ * <p>
+ * We cannot do this mid-render - actual attachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param surface InputSurface to initialize our child Node with.
+ * @param effects DistortedEffects to initialize our child Node with.
+ * @param mesh MeshObject to initialize our child Node with.
+ * @return the newly constructed child Node, or null if we couldn't allocate resources.
+ */
+  public DistortedNode attach(DistortedInputSurface surface, DistortedEffects effects, MeshObject mesh)
+    {
+    DistortedNode node = new DistortedNode(surface,effects,mesh);
+    mJobs.add(new Job(ATTACH,node,null));
+    DistortedMaster.newSlave(this);
+    return node;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes the first occurrence of a specified child from the list of children of our Surface.
+ * <p>
+ * A bit questionable method as there can be many different Nodes attached as children, some
+ * of them having the same Effects but - for instance - different Mesh. Use with care.
+ * <p>
+ * We cannot do this mid-render - actual detachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param effects DistortedEffects to remove.
+ */
+  public void detach(DistortedEffects effects)
+    {
+    long id = effects.getID();
+    DistortedNode node;
+    boolean detached = false;
+
+    for(int i=0; i<mNumChildren; i++)
+      {
+      node = mChildren.get(i);
+
+      if( node.getEffects().getID()==id )
+        {
+        detached = true;
+        mJobs.add(new Job(DETACH,node,null));
+        DistortedMaster.newSlave(this);
+        break;
+        }
+      }
+
+    if( !detached )
+      {
+      // if we failed to detach any, it still might be the case that
+      // there's an ATTACH job that we need to cancel.
+      int num = mJobs.size();
+      Job job;
+
+      for(int i=0; i<num; i++)
+        {
+        job = mJobs.get(i);
+
+        if( job.type==ATTACH && job.node.getEffects()==effects )
+          {
+          mJobs.remove(i);
+          break;
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes the first occurrence of a specified child from the list of children of our Surface.
+ * <p>
+ * We cannot do this mid-render - actual attachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ *
+ * @param node The Node to remove.
+ */
+  public void detach(DistortedNode node)
+    {
+    mJobs.add(new Job(DETACH,node,null));
+    DistortedMaster.newSlave(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Removes all children Nodes.
+ * <p>
+ * We cannot do this mid-render - actual attachment will be done just before the next render, by the
+ * DistortedMaster (by calling doWork())
+ */
+  public void detachAll()
+    {
+    mJobs.add(new Job(DETALL,null,null));
+    DistortedMaster.newSlave(this);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * This is not really part of the public API. Has to be public only because it is a part of the
+ * DistortedSlave interface, which should really be a class that we extend here instead but
+ * Java has no multiple inheritance.
+ *
+ * @y.exclude
+ */
+  public void doWork()
+    {
+    int num = mJobs.size();
+    Job job;
+
+    for(int i=0; i<num; i++)
+      {
+      job = mJobs.remove(0);
+
+      switch(job.type)
+        {
+        case ATTACH: if( mChildren==null ) mChildren = new ArrayList<>(2);
+                     job.node.setSurfaceParent(this);
+                     DistortedMaster.addSorted(mChildren,job.node);
+                     mNumChildren++;
+                     break;
+        case DETACH: if( mNumChildren>0 && mChildren.remove(job.node) )
+                       {
+                       job.node.setSurfaceParent(null);
+                       mNumChildren--;
+                       }
+                     break;
+        case DETALL: if( mNumChildren>0 )
+                       {
+                       DistortedNode tmp;
+
+                       for(int j=mNumChildren-1; j>=0; j--)
+                         {
+                         tmp = mChildren.remove(j);
+                         tmp.setSurfaceParent(null);
+                         }
+
+                       mNumChildren = 0;
+                       }
+                     break;
+        case SORT  : job.node.setPost(job.dep);
+                     mChildren.remove(job.node);
+                     DistortedMaster.addSorted(mChildren,job.node);
+                     break;
+        }
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/library/main/DistortedRenderState.java b/src/main/java/org/distorted/library/main/DistortedRenderState.java
new file mode 100644
index 0000000..ff31bf1
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedRenderState.java
@@ -0,0 +1,570 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.opengl.GLES30;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Remember the OpenGL state.
+ * <p>
+ * This is a member of DistortedNode. Remembers the OpenGL state we want to set just before rendering
+ * the Node.
+ */
+public class DistortedRenderState
+{
+  // TODO: figure this out dynamically; this assumes 8 bit stencil buffer.
+  private static final int STENCIL_MASK = (1<<8)-1;
+
+  private static class RenderState
+    {
+    private int colorMaskR, colorMaskG, colorMaskB, colorMaskA;
+    private int depthMask;
+    private int stencilMask;
+    private int depthTest;
+    private int stencilTest;
+    private int stencilFuncFunc, stencilFuncRef, stencilFuncMask;
+    private int stencilOpSfail, stencilOpDpfail, stencilOpDppass;
+    private int depthFunc;
+    private int blend;
+    private int blendSrc, blendDst;
+    }
+
+  private RenderState mState;          // state the current object wants to have
+  static private RenderState cState = new RenderState();   // current OpenGL Stave
+  static private RenderState sState = new RenderState();   // saved OpenGL state
+
+  private int mClear;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// default: color writes on, depth test and writes on, blending on, stencil off.
+
+  DistortedRenderState()
+    {
+    mState = new RenderState();
+
+    mState.colorMaskR = 1;
+    mState.colorMaskG = 1;
+    mState.colorMaskB = 1;
+    mState.colorMaskA = 1;
+
+    mState.depthTest  = 1;
+    mState.depthMask  = 1;
+    mState.depthFunc  = GLES30.GL_LEQUAL;
+
+    mState.blend      = 1;
+    mState.blendSrc   = GLES30.GL_SRC_ALPHA;
+    mState.blendDst   = GLES30.GL_ONE_MINUS_SRC_ALPHA;
+
+    mState.stencilTest     = 0;
+    mState.stencilMask     = STENCIL_MASK;
+    mState.stencilFuncFunc = GLES30.GL_NEVER;
+    mState.stencilFuncRef  = 0;
+    mState.stencilFuncMask = STENCIL_MASK;
+    mState.stencilOpSfail  = GLES30.GL_KEEP;
+    mState.stencilOpDpfail = GLES30.GL_KEEP;
+    mState.stencilOpDppass = GLES30.GL_KEEP;
+
+    mClear = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// reset state of everything to 'unknown'
+
+  static void reset()
+    {
+    cState.colorMaskR = -1;
+    cState.colorMaskG = -1;
+    cState.colorMaskB = -1;
+    cState.colorMaskA = -1;
+
+    cState.depthTest  = -1;
+    cState.depthMask  = -1;
+    cState.depthFunc  = -1;
+
+    cState.blend      = -1;
+    cState.blendSrc   = -1;
+    cState.blendDst   = -1;
+
+    cState.stencilTest     = -1;
+    cState.stencilMask     = -1;
+    cState.stencilFuncFunc = -1;
+    cState.stencilFuncRef  = -1;
+    cState.stencilFuncMask = -1;
+    cState.stencilOpSfail  = -1;
+    cState.stencilOpDpfail = -1;
+    cState.stencilOpDppass = -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void colorDepthStencilOn()
+    {
+    if( cState.colorMaskR!=1 || cState.colorMaskG!=1 || cState.colorMaskB!=1 || cState.colorMaskA!=1 )
+      {
+      sState.colorMaskR = cState.colorMaskR;
+      sState.colorMaskG = cState.colorMaskG;
+      sState.colorMaskB = cState.colorMaskB;
+      sState.colorMaskA = cState.colorMaskA;
+      cState.colorMaskR = 1;
+      cState.colorMaskG = 1;
+      cState.colorMaskB = 1;
+      cState.colorMaskA = 1;
+      GLES30.glColorMask(true,true,true,true);
+      }
+    if( cState.depthMask!=1 )
+      {
+      sState.depthMask = cState.depthMask;
+      cState.depthMask = 1;
+      GLES30.glDepthMask(true);
+      }
+    if( cState.stencilMask!= STENCIL_MASK )
+      {
+      sState.stencilMask = cState.stencilMask;
+      cState.stencilMask = STENCIL_MASK;
+      GLES30.glStencilMask(cState.stencilMask);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void switchOffDrawing()
+    {
+    GLES30.glEnable(GLES30.GL_SCISSOR_TEST);
+    GLES30.glScissor(0,0,0,0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void restoreDrawing()
+    {
+    GLES30.glDisable(GLES30.GL_SCISSOR_TEST);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void setUpStencilMark()
+    {
+    if( cState.stencilTest!=1 )
+      {
+      sState.stencilTest = cState.stencilTest;
+      cState.stencilTest = 1;
+      //android.util.Log.d("State", "stencil test on");
+      GLES30.glEnable(GLES30.GL_STENCIL_TEST);
+      }
+    if( cState.stencilFuncFunc!=GLES30.GL_ALWAYS || cState.stencilFuncRef!=1 || cState.stencilFuncMask!=STENCIL_MASK )
+      {
+      sState.stencilFuncFunc = cState.stencilFuncFunc;
+      sState.stencilFuncRef  = cState.stencilFuncRef;
+      sState.stencilFuncMask = cState.stencilFuncMask;
+      cState.stencilFuncFunc = GLES30.GL_ALWAYS;
+      cState.stencilFuncRef  = 1;
+      cState.stencilFuncMask = STENCIL_MASK;
+      //android.util.Log.d("State", "stencil func on");
+      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
+      }
+    if( cState.stencilOpSfail!=GLES30.GL_KEEP || cState.stencilOpDpfail!=GLES30.GL_KEEP || cState.stencilOpDppass!=GLES30.GL_REPLACE )
+      {
+      sState.stencilOpSfail = cState.stencilOpSfail;
+      sState.stencilOpDpfail= cState.stencilOpDpfail;
+      sState.stencilOpDppass= cState.stencilOpDppass;
+      cState.stencilOpSfail = GLES30.GL_KEEP;
+      cState.stencilOpDpfail= GLES30.GL_KEEP;
+      cState.stencilOpDppass= GLES30.GL_REPLACE;
+      //android.util.Log.d("State", "stencil op on");
+      GLES30.glStencilOp(cState.stencilOpSfail,cState.stencilOpDpfail,cState.stencilOpDppass);
+      }
+    if( cState.colorMaskR!=0 || cState.colorMaskG!=0 || cState.colorMaskB!=0 || cState.colorMaskA!=0 )
+      {
+      sState.colorMaskR = cState.colorMaskR;
+      sState.colorMaskG = cState.colorMaskG;
+      sState.colorMaskB = cState.colorMaskB;
+      sState.colorMaskA = cState.colorMaskA;
+      cState.colorMaskR = 0;
+      cState.colorMaskG = 0;
+      cState.colorMaskB = 0;
+      cState.colorMaskA = 0;
+      //android.util.Log.d("State", "switch off color writing");
+      GLES30.glColorMask(false,false,false,false);
+      }
+    if( cState.depthMask!=1 )
+      {
+      sState.depthMask = cState.depthMask;
+      cState.depthMask = 1;
+      //android.util.Log.d("State", "switch on depth writing");
+      GLES30.glDepthMask(true);
+      }
+    if( cState.stencilMask!= STENCIL_MASK )
+      {
+      sState.stencilMask = cState.stencilMask;
+      cState.stencilMask = STENCIL_MASK;
+      //android.util.Log.d("State", "stencil mask on");
+      GLES30.glStencilMask(cState.stencilMask);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void unsetUpStencilMark()
+    {
+    if( sState.stencilTest!=cState.stencilTest )
+      {
+      cState.stencilTest = sState.stencilTest;
+
+      if (cState.stencilTest == 0)
+        {
+        GLES30.glDisable(GLES30.GL_STENCIL_TEST);
+        }
+      else
+        {
+        GLES30.glEnable(GLES30.GL_STENCIL_TEST);
+        }
+      }
+    if( sState.colorMaskR!=cState.colorMaskR || sState.colorMaskG!=cState.colorMaskG || sState.colorMaskB!=cState.colorMaskB || sState.colorMaskA!=cState.colorMaskA)
+      {
+      cState.colorMaskR = sState.colorMaskR;
+      cState.colorMaskG = sState.colorMaskG;
+      cState.colorMaskB = sState.colorMaskB;
+      cState.colorMaskA = sState.colorMaskA;
+      GLES30.glColorMask(cState.colorMaskR==1,cState.colorMaskG==1,cState.colorMaskB==1,cState.colorMaskA==1);
+      }
+    if( sState.depthMask!=cState.depthMask )
+      {
+      cState.depthMask = sState.depthMask;
+      GLES30.glDepthMask(cState.depthMask==1);
+      }
+    if( sState.stencilMask!=cState.stencilMask )
+      {
+      cState.stencilMask = sState.stencilMask;
+      GLES30.glStencilMask(cState.stencilMask);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void useStencilMark()
+    {
+    if( cState.stencilTest!=1 )
+      {
+      sState.stencilTest = cState.stencilTest;
+      cState.stencilTest = 1;
+      GLES30.glEnable(GLES30.GL_STENCIL_TEST);
+      }
+    if( cState.stencilFuncFunc!=GLES30.GL_EQUAL || cState.stencilFuncRef!=1 || cState.stencilFuncMask!=STENCIL_MASK )
+      {
+      sState.stencilFuncFunc = cState.stencilFuncFunc;
+      sState.stencilFuncRef  = cState.stencilFuncRef;
+      sState.stencilFuncMask = cState.stencilFuncMask;
+      cState.stencilFuncFunc = GLES30.GL_EQUAL;
+      cState.stencilFuncRef  = 1;
+      cState.stencilFuncMask = STENCIL_MASK;
+      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
+      }
+    if( cState.stencilMask!= 0x00 )
+      {
+      sState.stencilMask = cState.stencilMask;
+      cState.stencilMask = 0x00;
+      GLES30.glStencilMask(cState.stencilMask);
+      }
+     if( cState.depthMask!=0 )
+      {
+      sState.depthMask = cState.depthMask;
+      cState.depthMask = 0;
+      GLES30.glDepthMask(false);
+      }
+    if( cState.depthTest!=0 )
+      {
+      sState.depthTest = cState.depthTest;
+      cState.depthTest = 0;
+      GLES30.glDisable(GLES30.GL_DEPTH_TEST);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void unuseStencilMark()
+    {
+    if( sState.stencilTest!=cState.stencilTest )
+      {
+      cState.stencilTest = sState.stencilTest;
+
+      if (cState.stencilTest == 0)
+        {
+        GLES30.glDisable(GLES30.GL_STENCIL_TEST);
+        }
+      else
+        {
+        GLES30.glEnable(GLES30.GL_STENCIL_TEST);
+        }
+      }
+    if( sState.stencilFuncFunc!=cState.stencilFuncFunc || sState.stencilFuncRef!=cState.stencilFuncRef || sState.stencilFuncMask!=cState.stencilFuncMask )
+      {
+      cState.stencilFuncFunc = sState.stencilFuncFunc;
+      cState.stencilFuncRef  = sState.stencilFuncRef ;
+      cState.stencilFuncMask = sState.stencilFuncMask;
+      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
+      }
+    if( sState.stencilMask!=cState.stencilMask )
+      {
+      cState.stencilMask = sState.stencilMask;
+      GLES30.glStencilMask(cState.stencilMask);
+      }
+    if( sState.depthMask!=cState.depthMask )
+      {
+      cState.depthMask = sState.depthMask;
+      GLES30.glDepthMask(cState.depthMask==1);
+      }
+    if( sState.depthTest!=cState.depthTest )
+      {
+      cState.depthTest = sState.depthTest;
+
+      if (cState.depthTest == 0)
+        {
+        GLES30.glDisable(GLES30.GL_DEPTH_TEST);
+        }
+      else
+        {
+        GLES30.glEnable(GLES30.GL_DEPTH_TEST);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void apply()
+    {
+    //android.util.Log.e("State", "APPLYING STATE");
+
+    /////////////////////////////////////////////////////
+    // 1. Write to color buffer?
+    if( mState.colorMaskR!=cState.colorMaskR || mState.colorMaskG!=cState.colorMaskG || mState.colorMaskB!=cState.colorMaskB || mState.colorMaskA!=cState.colorMaskA)
+      {
+      //android.util.Log.d("State", "setting color mask");
+      cState.colorMaskR = mState.colorMaskR;
+      cState.colorMaskG = mState.colorMaskG;
+      cState.colorMaskB = mState.colorMaskB;
+      cState.colorMaskA = mState.colorMaskA;
+      GLES30.glColorMask(cState.colorMaskR==1,cState.colorMaskG==1,cState.colorMaskB==1,cState.colorMaskA==1);
+      }
+
+    /////////////////////////////////////////////////////
+    // 2. Enable Depth test?
+    if( mState.depthTest!=cState.depthTest )
+      {
+      cState.depthTest = mState.depthTest;
+
+      if (cState.depthTest == 0)
+        {
+        //android.util.Log.d("State", "disabling depth test");
+        GLES30.glDisable(GLES30.GL_DEPTH_TEST);
+        }
+      else
+        {
+        //android.util.Log.d("State", "enable depth test");
+        GLES30.glEnable(GLES30.GL_DEPTH_TEST);
+        }
+      }
+
+    /////////////////////////////////////////////////////
+    // 3. Change Depth Function?
+    if( mState.depthFunc!=cState.depthFunc )
+      {
+      //android.util.Log.d("State", "setting depth func");
+      cState.depthFunc = mState.depthFunc;
+      GLES30.glDepthFunc(cState.depthFunc);
+      }
+
+    /////////////////////////////////////////////////////
+    // 4. Write to Depth buffer?
+    if( mState.depthMask!=cState.depthMask )
+      {
+      //android.util.Log.d("State", "setting depth mask");
+      cState.depthMask = mState.depthMask;
+      GLES30.glDepthMask(cState.depthMask==1);
+      }
+
+    /////////////////////////////////////////////////////
+    // 5. Enable Blending?
+    if( mState.blend!=cState.blend )
+      {
+      cState.blend = mState.blend;
+
+      if (cState.blend == 0)
+        {
+        //android.util.Log.d("State", "disabling blending");
+        GLES30.glDisable(GLES30.GL_BLEND);
+        }
+      else
+        {
+        //android.util.Log.d("State", "enabling blending");
+        GLES30.glEnable(GLES30.GL_BLEND);
+        }
+      }
+
+    /////////////////////////////////////////////////////
+    // 6. Change Blend function?
+    if( mState.blendSrc!=cState.blendSrc || mState.blendDst!=cState.blendDst )
+      {
+      //android.util.Log.d("State", "setting blend function");
+      cState.blendSrc = mState.blendSrc;
+      cState.blendDst = mState.blendDst;
+      GLES30.glBlendFunc(cState.blendSrc,cState.blendDst);
+      }
+
+    /////////////////////////////////////////////////////
+    // 7. Enable/Disable Stencil Test?
+    if( mState.stencilTest!=cState.stencilTest )
+      {
+      cState.stencilTest = mState.stencilTest;
+
+      if (cState.stencilTest == 0)
+        {
+        //android.util.Log.d("State", "disabling stencil test");
+        GLES30.glDisable(GLES30.GL_STENCIL_TEST);
+        }
+      else
+        {
+        //android.util.Log.d("State", "enabling stencil test");
+        GLES30.glEnable(GLES30.GL_STENCIL_TEST);
+        }
+      }
+
+    /////////////////////////////////////////////////////
+    // 8. Adjust Stencil function?
+    if( mState.stencilFuncFunc!=cState.stencilFuncFunc || mState.stencilFuncRef!=cState.stencilFuncRef || mState.stencilFuncMask!=cState.stencilFuncMask )
+      {
+      //android.util.Log.d("State", "setting stencil function");
+      cState.stencilFuncFunc = mState.stencilFuncFunc;
+      cState.stencilFuncRef  = mState.stencilFuncRef ;
+      cState.stencilFuncMask = mState.stencilFuncMask;
+      GLES30.glStencilFunc(cState.stencilFuncFunc,cState.stencilFuncRef,cState.stencilFuncMask);
+      }
+
+    /////////////////////////////////////////////////////
+    // 9. Adjust Stencil operation?
+    if( mState.stencilOpSfail!=cState.stencilOpSfail || mState.stencilOpDpfail!=cState.stencilOpDpfail || mState.stencilOpDppass!=cState.stencilOpDppass )
+      {
+      //android.util.Log.d("State", "setting stencil op");
+      cState.stencilOpSfail = mState.stencilOpSfail;
+      cState.stencilOpDpfail= mState.stencilOpDpfail;
+      cState.stencilOpDppass= mState.stencilOpDppass;
+      GLES30.glStencilOp(cState.stencilOpSfail,cState.stencilOpDpfail,cState.stencilOpDppass);
+      }
+
+    /////////////////////////////////////////////////////
+    // 10. Write to Stencil buffer?
+    if( mState.stencilMask!=cState.stencilMask )
+      {
+      //android.util.Log.d("State", "setting stencil mask");
+      cState.stencilMask = mState.stencilMask;
+      GLES30.glStencilMask(cState.stencilMask);
+      }
+
+    /////////////////////////////////////////////////////
+    // 11. Clear buffers?
+    if( mClear!=0 )
+      {
+      //android.util.Log.d("State", "clearing buffer");
+      GLES30.glClear(mClear);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glColorMask(boolean r, boolean g, boolean b, boolean a)
+    {
+    mState.colorMaskR = (r ? 1:0);
+    mState.colorMaskG = (g ? 1:0);
+    mState.colorMaskB = (b ? 1:0);
+    mState.colorMaskA = (a ? 1:0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glDepthMask(boolean mask)
+    {
+    mState.depthMask = (mask ? 1:0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glStencilMask(int mask)
+    {
+    mState.stencilMask = mask;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glEnable(int test)
+    {
+         if( test==GLES30.GL_DEPTH_TEST   ) mState.depthTest   = 1;
+    else if( test==GLES30.GL_STENCIL_TEST ) mState.stencilTest = 1;
+    else if( test==GLES30.GL_BLEND        ) mState.blend       = 1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glDisable(int test)
+    {
+         if( test==GLES30.GL_DEPTH_TEST   ) mState.depthTest   = 0;
+    else if( test==GLES30.GL_STENCIL_TEST ) mState.stencilTest = 0;
+    else if( test==GLES30.GL_BLEND        ) mState.blend       = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glStencilFunc(int func, int ref, int mask)
+    {
+    mState.stencilFuncFunc = func;
+    mState.stencilFuncRef  = ref;
+    mState.stencilFuncMask = mask;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glStencilOp(int sfail, int dpfail, int dppass)
+    {
+    mState.stencilOpSfail = sfail;
+    mState.stencilOpDpfail= dpfail;
+    mState.stencilOpDppass= dppass;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glDepthFunc(int func)
+    {
+    mState.depthFunc = func;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glBlendFunc(int src, int dst)
+    {
+    mState.blendSrc = src;
+    mState.blendDst = dst;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void glClear(int mask)
+    {
+    mClear = mask;
+    }
+}
diff --git a/src/main/java/org/distorted/library/main/DistortedScreen.java b/src/main/java/org/distorted/library/main/DistortedScreen.java
new file mode 100644
index 0000000..be516ea
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedScreen.java
@@ -0,0 +1,146 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+
+import org.distorted.library.type.Static3D;
+
+/**
+ * Class which represents the Screen.
+ * <p>
+ * User is able to render to it just like to a DistortedFramebuffer.
+ */
+public class DistortedScreen extends DistortedOutputSurface
+  {
+  ///// DEBUGGING ONLY /////////////////////////
+  private static final int NUM_FRAMES  = 100;
+
+  private MeshObject fpsMesh;
+  private DistortedTexture fpsTexture;
+  private DistortedEffects fpsEffects;
+  private Canvas fpsCanvas;
+  private Bitmap fpsBitmap;
+  private Paint mPaint;
+  private int fpsH, fpsW;
+  private String fpsString = "";
+  private long lastTime=0;
+  private long[] durations;
+  private int currDuration;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// here we don't manage underlying OpenGL assets ourselves
+
+  void create()   {}
+  void delete()   {}
+  void recreate() {}
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void prepareDebug(long time)
+    {
+    if( lastTime==0 ) lastTime = time;
+
+    currDuration++;
+    if (currDuration >= NUM_FRAMES) currDuration = 0;
+    durations[NUM_FRAMES] += ((time - lastTime) - durations[currDuration]);
+    durations[currDuration] = time - lastTime;
+
+    fpsString = "" + ((int)(10000.0f*NUM_FRAMES/durations[NUM_FRAMES]))/10.0f;
+
+    mPaint.setColor(0xffffffff);
+    fpsCanvas.drawRect(0, 0, fpsW, fpsH, mPaint);
+    mPaint.setColor(0xff000000);
+    fpsCanvas.drawText(fpsString, fpsW/2, 0.75f*fpsH, mPaint);
+    fpsTexture.setTexture(fpsBitmap);
+
+    lastTime = time;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void renderDebug(long time)
+    {
+    if (fpsTexture.setAsInput())
+      {
+      setAsOutput(time);
+      GLES30.glColorMask(true,true,true,true);
+      GLES30.glDepthMask(false);
+      GLES30.glDisable(GLES30.GL_STENCIL_TEST);
+      GLES30.glDisable(GLES30.GL_DEPTH_TEST);
+      fpsEffects.drawPriv(fpsW/2.0f, fpsH/2.0f, fpsMesh, this, time, 0);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create a new Screen.
+ * <p>
+ * Has to be followed by a 'resize()' to set the size.
+ */
+  public DistortedScreen(GLSurfaceView view)
+    {
+    // set color to 'DONT_CREATE' so that Screens will not get added to the Surface lists
+    // set depth to 'CREATED' so that depth will be, by default, on.
+    super(0,0,DONT_CREATE,1,DEPTH_NO_STENCIL,0,TYPE_USER);
+
+    if( view!=null )
+      {
+      Context context = view.getContext();
+      final ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+      final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+      view.setEGLContextClientVersion((configurationInfo.reqGlEsVersion >> 16) >= 3 ? 3 : 2);
+      }
+
+    /////// DEBUGGING ONLY //////////////
+    fpsW = 120;
+    fpsH =  70;
+
+    fpsBitmap = Bitmap.createBitmap(fpsW,fpsH, Bitmap.Config.ARGB_8888);
+    fpsMesh = new MeshFlat(1,1);
+    fpsTexture = new DistortedTexture(fpsW,fpsH);
+    fpsTexture.setTexture(fpsBitmap);
+    fpsCanvas = new Canvas(fpsBitmap);
+    fpsEffects = new DistortedEffects();
+    fpsEffects.move( new Static3D(5,5,0) );
+
+    mPaint = new Paint();
+    mPaint.setAntiAlias(true);
+    mPaint.setTextAlign(Paint.Align.CENTER);
+    mPaint.setTextSize(0.7f*fpsH);
+
+    durations = new long[NUM_FRAMES+1];
+    currDuration = 0;
+
+    for(int i=0; i<NUM_FRAMES+1; i++) durations[i]=16;  // Assume FPS will be
+    durations[NUM_FRAMES] = NUM_FRAMES*16;              // close to 1000/16 ~ 60
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/DistortedSlave.java b/src/main/java/org/distorted/library/main/DistortedSlave.java
new file mode 100644
index 0000000..b9d3c6c
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedSlave.java
@@ -0,0 +1,37 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Package-private interface implemented by all objects which can be assigned work by the DistortedMaster.
+ * <p>
+ * All the methods below are really meant to be package-local; and this interface should really be a
+ * package-local class which other classes would extend (but that's impossible because OutputSurface
+ * already extends other class).
+ */
+interface DistortedSlave
+  {
+  /**
+   * Not part of public API, do not document
+   * @y.exclude
+   */
+  void doWork();
+  }
diff --git a/src/main/java/org/distorted/library/main/DistortedSurface.java b/src/main/java/org/distorted/library/main/DistortedSurface.java
new file mode 100644
index 0000000..ed982b7
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedSurface.java
@@ -0,0 +1,95 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+abstract class DistortedSurface extends DistortedObject
+{
+  int mColorCreated;
+  int mNumColors;
+  int[] mColorH;
+  int mWidth, mHeight;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  DistortedSurface(int width, int height, int create, int numcolors, int type)
+    {
+    super(create,type);
+
+    mNumColors    = numcolors;
+    mWidth        = width ;
+    mHeight       = height;
+    mColorCreated = create;
+
+    if( mNumColors>0 )
+      {
+      mColorH = new int[mNumColors];
+      for( int i=0; i<mNumColors; i++ )  mColorH[i] = 0;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// debugging only
+
+  String printDetails()
+    {
+    return getClass().getSimpleName()+" "+mWidth+"x"+mHeight;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Return the width of this Surface.
+ *
+ * @return width of the Object, in pixels.
+ */
+  public int getWidth()
+    {
+    return mWidth;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Return the height of this Surface.
+ *
+ * @return height of the Object, in pixels.
+ */
+  public int getHeight()
+    {
+    return mHeight;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Return the depth of this Surface.
+ * <p>
+ * Admittedly quite a strange method. Why do we need to pass a Mesh to it? Because one cannot determine
+ * 'depth' of a Surface (bitmap really!) when rendered based only on the texture itself, that depends
+ * on the Mesh it is rendered with.
+ *
+ * @return depth of the Object, in pixels.
+ */
+  public int getDepth(MeshObject mesh)
+    {
+    return mesh==null ? 0 : (int)(mWidth*mesh.zFactor);
+    }
+}
diff --git a/src/main/java/org/distorted/library/main/DistortedTexture.java b/src/main/java/org/distorted/library/main/DistortedTexture.java
new file mode 100644
index 0000000..a572d8a
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/DistortedTexture.java
@@ -0,0 +1,183 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.opengl.GLES30;
+import android.opengl.GLUtils;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Class which represents a OpenGL Texture object.
+ * <p>
+ * Create a Texture of arbitrary size and feed some pixels to it via the setTexture() method.
+ */
+public class DistortedTexture extends DistortedSurface implements DistortedInputSurface
+  {
+  private Bitmap mBmp= null;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// We have to flip vertically every single Bitmap that we get fed with.
+//
+// Reason: textures read from files are the only objects in OpenGL which have their origins at the
+// upper-left corner. Everywhere else the origin is in the lower-left corner. Thus we have to flip.
+// The alternative solution, namely inverting the y-coordinate of the TexCoord does not really work-
+// i.e. it works only in case of rendering directly to the screen, but if we render to an FBO and
+// then take the FBO and render to screen, (DistortedNode does so!) things get inverted as textures
+// created from FBO have their origins in the lower-left... Mindfuck!
+
+  private static Bitmap flipBitmap(Bitmap src)
+    {
+    Matrix matrix = new Matrix();
+    matrix.preScale(1.0f,-1.0f);
+
+    return Bitmap.createBitmap(src,0,0,src.getWidth(),src.getHeight(), matrix,true);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// must be called from a thread holding OpenGL Context
+
+  void create()
+    {
+    if( mBmp!=null )
+      {
+      if( mColorCreated==NOT_CREATED_YET )
+        {
+        mColorCreated = CREATED;
+        GLES30.glGenTextures(1, mColorH, 0);
+        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
+        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MIN_FILTER, GLES30.GL_LINEAR );
+        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_MAG_FILTER, GLES30.GL_LINEAR );
+        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_S, GLES30.GL_CLAMP_TO_EDGE );
+        GLES30.glTexParameteri ( GLES30.GL_TEXTURE_2D, GLES30.GL_TEXTURE_WRAP_T, GLES30.GL_CLAMP_TO_EDGE );
+        GLUtils.texImage2D(GLES30.GL_TEXTURE_2D, 0, flipBitmap(mBmp), 0);
+        }
+      else
+        {
+        GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
+        GLUtils.texSubImage2D(GLES30.GL_TEXTURE_2D, 0,0,0,flipBitmap(mBmp));
+        }
+
+      mBmp = null;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// must be called from a thread holding OpenGL Context
+
+  void delete()
+    {
+    if( mColorH[0]>0 )
+      {
+      GLES30.glDeleteTextures(1, mColorH, 0);
+      mColorH[0] = 0;
+      mColorCreated = NOT_CREATED_YET;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// called from onDestroy(); mark OpenGL assets as 'not created'
+
+  void recreate()
+    {
+    if( mColorCreated!=DONT_CREATE )
+      {
+      mColorCreated = NOT_CREATED_YET;
+      mColorH[0] = 0;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// create SYSTEM or TREE textures (those are just like normal Textures, just hold information
+// that they were autocreated only for the Library's internal purposes (SYSTEM) or for using
+// inside a Tree of DistortedNodes (TREE)
+// SYSTEM surfaces do not get removed in onDestroy().
+
+  public DistortedTexture(int width, int height, int type)
+    {
+    super(width,height,NOT_CREATED_YET,1,type);
+    mBmp= null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create empty texture of given dimensions.
+ */
+  public DistortedTexture(int width, int height)
+    {
+    super(width,height,NOT_CREATED_YET,1,TYPE_USER);
+    mBmp= null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Bind the underlying rectangle of pixels as a OpenGL Texture.
+ */
+  public boolean setAsInput()
+    {
+    if( mColorH[0]>0 )
+      {
+      GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+      GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, mColorH[0]);
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Sets the underlying android.graphics.Bitmap object.
+ * <p>
+ * You can only recycle() the passed Bitmap once the OpenGL context gets created (i.e. after call
+ * to GLSurfaceView.onSurfaceCreated) because only after this point can the Library upload it to the GPU!
+ *
+ * @param bmp The android.graphics.Bitmap object to apply effects to and display.
+ */
+  public void setTexture(Bitmap bmp)
+    {
+    mBmp= bmp;
+    markForCreation();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Paints the Texture with solid color.
+ *
+ * @param argb The color to paint the Texture with.
+ */
+  public void setColor(int argb)
+    {
+    Paint paint = new Paint();
+    paint.setColor(argb);
+    paint.setStyle(Paint.Style.FILL);
+
+    mBmp = Bitmap.createBitmap(1,1, Bitmap.Config.ARGB_8888);
+    Canvas canvas = new Canvas(mBmp);
+    canvas.drawRect(0,0,1,1,paint);
+
+    markForCreation();
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/EffectMessageSender.java b/src/main/java/org/distorted/library/main/EffectMessageSender.java
new file mode 100644
index 0000000..1155f20
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/EffectMessageSender.java
@@ -0,0 +1,143 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.message.EffectMessage;
+
+import java.util.Vector;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+final class EffectMessageSender extends Thread
+  {
+  private class Message
+    {
+    EffectListener mListener;
+    EffectMessage mMessage;
+    long mEffectID;
+    int mEffectName;
+    long mBitmapID;
+
+    Message(EffectListener l, EffectMessage m, long id, int name, long bmpID)
+      {
+      mListener   = l;
+      mMessage    = m;
+      mEffectID   = id;
+      mEffectName = name;
+      mBitmapID   = bmpID;
+      }
+    }
+  
+  private static Vector<Message> mList =null;
+  private static EffectMessageSender mThis=null;
+  private static volatile boolean mNotify  = false;
+  private static volatile boolean mRunning = false;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  private EffectMessageSender() 
+    {
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void startSending()
+    {
+    mRunning = true;
+
+    if( mThis==null )
+      {
+      mList = new Vector<>();
+      mThis = new EffectMessageSender();
+      mThis.start();
+      }
+    else  
+      {  
+      synchronized(mThis)
+        {
+        mThis.notify();
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  static void stopSending()
+    {
+    mRunning = false;
+
+    if( mThis!=null )
+      {
+      synchronized(mThis)
+        {
+        mThis.notify();
+        }
+      }
+    }
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  public void run()
+    {
+    Message tmp;  
+     
+    while(mRunning)
+      {
+      //android.util.Log.i("SENDER", "sender thread running...");
+
+      while( mList.size()>0 )
+        {
+        tmp = mList.remove(0);
+        tmp.mListener.effectMessage(tmp.mMessage, tmp.mEffectID, tmp.mEffectName,tmp.mBitmapID);
+        }
+
+      synchronized(mThis)
+        {
+        if (!mNotify)
+          {
+          try  { mThis.wait(); }
+          catch(InterruptedException ex) { }
+          }
+        mNotify = false;
+        }
+      }
+
+    mThis = null;
+    mList.clear();
+
+    //android.util.Log.i("SENDER", "sender thread finished...");
+    }
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+        
+  static void newMessage(EffectListener l, EffectMessage m, long id, int name, long bmpID)
+    {
+    Message msg = mThis.new Message(l,m,id,name,bmpID);
+    mList.add(msg);
+
+    synchronized(mThis)
+      {
+      mNotify = true;
+      mThis.notify();
+      }
+    }
+  }
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/main/EffectQuality.java b/src/main/java/org/distorted/library/main/EffectQuality.java
new file mode 100644
index 0000000..23085cc
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/EffectQuality.java
@@ -0,0 +1,50 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * A list of quality levels.
+ * <p>
+ * One can set quality of a Postprocessing Effect to one of those. The lower the quality, the faster
+ * the rendering will be.
+ *
+ * @see DistortedEffectsPostprocess
+ */
+public enum EffectQuality
+  {
+  HIGHEST  ( 0 ),   // has to start from 0
+  HIGH     ( 1 ),
+  MEDIUM   ( 2 ),
+  LOW      ( 3 );
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  final static float MULTIPLIER = 0.5f;      // each next Quality level renders into 1/0.5 smaller buffers
+  final static int LENGTH = values().length;
+
+  final int level;
+
+  EffectQuality(int level)
+    {
+    this.level = level;
+    }
+  }
+
diff --git a/src/main/java/org/distorted/library/main/EffectQueue.java b/src/main/java/org/distorted/library/main/EffectQueue.java
new file mode 100644
index 0000000..611a711
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/EffectQueue.java
@@ -0,0 +1,366 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.type.Dynamic;
+
+import java.util.Vector;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+abstract class EffectQueue
+  {
+  protected byte mNumEffects;   // number of effects at the moment
+  protected long mTotalEffects; // total number of effects ever created
+  protected int[] mName;
+  protected int[] mType;
+  protected float[] mUniforms;
+  protected float[] mCache;
+  protected Dynamic[][] mInter;
+  protected long[] mCurrentDuration;
+  protected byte[] mFreeIndexes;
+  protected byte[] mIDIndex;
+  protected long[] mID;
+  protected long mTime=0;
+  protected static int[] mMax = new int[Effect.LENGTH];
+  protected int mMaxIndex;
+  protected Vector<EffectListener> mListeners =null;
+  protected int mNumListeners=0;  // ==mListeners.length(), but we only create mListeners if the first one gets added
+  protected long mObjectID;
+
+  private static boolean mCreated;
+
+  static
+    {
+    onDestroy();
+    }
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueue(long id, int numUniforms, int numCache, int index)
+    {
+    mNumEffects   = 0;
+    mTotalEffects = 0;
+    mMaxIndex     = index;
+    mObjectID     = id;
+
+    int max = mMax[mMaxIndex];
+
+    if( max>0 )
+      {
+      mName            = new int[max];
+      mType            = new int[max];
+      mUniforms        = new float[numUniforms*max];
+      mInter           = new Dynamic[3][max];
+      mCurrentDuration = new long[max];
+      mID              = new long[max];
+      mIDIndex         = new byte[max];
+      mFreeIndexes     = new byte[max];
+     
+      for(byte i=0; i<max; i++) mFreeIndexes[i] = i;
+
+      if( numCache>0 )
+        {
+        mCache = new float[numCache*max];
+        }
+      }
+   
+    mCreated = true;  
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @SuppressWarnings("unused")
+  int getNumEffects()
+    {
+    return mNumEffects;  
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Only max Byte.MAX_VALUE concurrent effects per DistortedEffects object.
+// If you want more, change type of the mNumEffects, mIDIndex and mFreeIndexes variables to shorts.
+// (although probably this many uniforms will not fit in the shaders anyway!)
+
+  static boolean setMax(int index, int m)
+    {
+    if( (!mCreated && !Distorted.isInitialized()) || m<=mMax[index] )
+      {
+      if( m<0              ) m = 0;
+      else if( m>Byte.MAX_VALUE ) m = Byte.MAX_VALUE;
+
+      mMax[index] = m;
+      return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static int getMax(int index)
+    {
+    return mMax[index];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void registerForMessages(EffectListener el)
+    {
+    if( mListeners==null ) mListeners = new Vector<>(2,2);
+
+    if( !mListeners.contains(el) )
+      {
+      mListeners.add(el);
+      mNumListeners++;
+      }
+    }
+ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void deregisterForMessages(EffectListener el)
+    {
+    if( mListeners.remove(el) )
+      {
+      mNumListeners--;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void onDestroy()
+    {
+    Effect.reset(mMax);
+    mCreated = false;  
+    }
+ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized int removeByID(long id)
+    {
+    int i = getEffectIndex(id);
+   
+    if( i>=0 ) 
+      {
+      remove(i);
+      return 1;
+      }
+   
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized int removeByType(Effect effect)
+    {
+    int ret  = 0;
+    int name = effect.getName();
+    int type = effect.getType();
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      if( mName[i]==name && mType[i]==type )
+        {
+        remove(i);
+        i--;
+        ret++;
+        }
+      }
+   
+    return ret;
+    }
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  private synchronized int getEffectIndex(long id)
+    {
+    int index = mIDIndex[(int)(id%mMax[mMaxIndex])];
+    return (index<mNumEffects && mID[index]==id ? index : -1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// we do want to notify Listeners if they called 'abortAll' themselves but don't want to notify
+// them if it is the library itself which is releasing resources.
+
+  synchronized int abortAll(boolean notify)
+    {
+    int ret = mNumEffects;
+    long removedID;
+    int removedName, removedType;
+
+    for(int i=0; i<ret; i++ )
+      {
+      mInter[0][i] = null;
+      mInter[1][i] = null;
+      mInter[2][i] = null;
+
+      if( notify )
+        {
+        removedID = mID[i];
+        removedName= mName[i];
+        removedType= mType[i];
+
+        for(int j=0; j<mNumListeners; j++)
+          EffectMessageSender.newMessage( mListeners.elementAt(j),
+                                          EffectMessage.EFFECT_REMOVED,
+                                          (removedID<<Effect.LENGTH)+removedType,
+                                          removedName,
+                                          mObjectID);
+        }
+      }
+
+    mNumEffects= 0;
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this assumes 0<=effect<mNumEffects
+  
+  protected void remove(int effect)
+    {
+    mNumEffects--;     
+    
+    byte removedIndex = (byte)(mID[effect]%mMax[mMaxIndex]);
+    byte removedPosition = mIDIndex[removedIndex];
+    mFreeIndexes[mNumEffects] = removedIndex;
+    
+    long removedID = mID[effect];
+    int removedName= mName[effect];
+    int removedType= mType[effect];
+
+    for(int j=0; j<mMax[mMaxIndex]; j++)
+      {
+      if( mIDIndex[j] > removedPosition ) mIDIndex[j]--; 
+      }
+         
+    for(int j=effect; j<mNumEffects; j++ ) 
+      {
+      mName[j]            = mName[j+1];
+      mType[j]            = mType[j+1];
+      mInter[0][j]        = mInter[0][j+1];
+      mInter[1][j]        = mInter[1][j+1];
+      mInter[2][j]        = mInter[2][j+1];
+      mCurrentDuration[j] = mCurrentDuration[j+1];
+      mID[j]              = mID[j+1];
+    
+      moveEffect(j);
+      }
+   
+    mInter[0][mNumEffects] = null;
+    mInter[1][mNumEffects] = null;
+    mInter[2][mNumEffects] = null;
+
+    for(int i=0; i<mNumListeners; i++) 
+      EffectMessageSender.newMessage( mListeners.elementAt(i),
+                                      EffectMessage.EFFECT_REMOVED,
+                                      (removedID<<Effect.LENGTH)+removedType,
+                                      removedName,
+                                      mObjectID);
+    }
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  protected long addBase(Effect effect)
+    {
+    int type = effect.getType();
+
+    mName[mNumEffects] = effect.getName();
+    mType[mNumEffects] = type;
+    mCurrentDuration[mNumEffects] = 0;
+    
+    int index = mFreeIndexes[mNumEffects];
+    long id = mTotalEffects*mMax[mMaxIndex] + index;
+    mID[mNumEffects] = id;
+    mIDIndex[index] = mNumEffects;  
+   
+    mNumEffects++; 
+    mTotalEffects++;
+   
+    return (id<<Effect.LENGTH)+type;
+    }
+    
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// used only for debugging
+
+  @SuppressWarnings("unused")
+  protected String printEffects(int max)
+    {
+    long[] indexes = new long[mMax[mMaxIndex]];
+   
+    for(int g=0; g<mMax[mMaxIndex]; g++)
+      {
+      indexes[g] = -1;  
+      }
+   
+    String ret="(";
+    int f;
+   
+    for(int g=0; g<max; g++) 
+      {
+      f = getEffectIndex(g);
+      if( f>=0 ) indexes[f] = g;
+      }
+   
+    for(int g=0; g<mMax[mMaxIndex]; g++)
+      {
+      ret += (g>0 ? ",":"")+(indexes[g]>=0 ? indexes[g] : " ");   
+      }
+   
+    ret += ")";
+   
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Only used for debugging
+  
+  protected boolean printByID(long id)
+    {
+    int index = getEffectIndex(id);
+   
+    if( index>=0 ) 
+      {
+      boolean inter0 = mInter[0][index]==null;
+      boolean inter1 = mInter[1][index]==null;
+      boolean inter2 = mInter[2][index]==null;
+
+      android.util.Log.e("EffectQueue", "numEffects="+mNumEffects+" effect id="+id+" index="+index+
+                         " duration="+mCurrentDuration[index]+" inter[0] null="+inter0+" inter[1] null="+inter1+" inter[2] null="+inter2);
+
+      if( !inter0 ) android.util.Log.e("EffectQueue","inter[0]: "+mInter[0][index].print());
+      if( !inter1 ) android.util.Log.e("EffectQueue","inter[1]: "+mInter[1][index].print());
+      if( !inter2 ) android.util.Log.e("EffectQueue","inter[2]: "+mInter[2][index].print());
+
+      return true;
+      }
+   
+    android.util.Log.e("EffectQueue", "effect id="+id+" not found");
+
+    return false;  
+    }
+ 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  abstract void moveEffect(int index);
+  }
+///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/library/main/EffectQueueFragment.java b/src/main/java/org/distorted/library/main/EffectQueueFragment.java
new file mode 100644
index 0000000..120a439
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/EffectQueueFragment.java
@@ -0,0 +1,213 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.opengl.GLES30;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.FragmentEffect;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.type.Dynamic4D;
+import org.distorted.library.type.Static;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.library.type.Static5D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class EffectQueueFragment extends EffectQueue
+  {
+  private static final int NUM_UNIFORMS = 8;
+  private static final int NUM_CACHE    = 4;
+  private static final int INDEX = Effect.FRAGMENT;
+  private static int mNumEffectsH;
+  private static int mTypeH;
+  private static int mUniformsH;
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueueFragment(long id)
+    { 
+    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void getUniforms(int mProgramH)
+    {
+    mNumEffectsH= GLES30.glGetUniformLocation( mProgramH, "fNumEffects");
+    mTypeH      = GLES30.glGetUniformLocation( mProgramH, "fType");
+    mUniformsH  = GLES30.glGetUniformLocation( mProgramH, "fUniforms");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  synchronized void compute(long currTime) 
+    { 
+    if( currTime==mTime ) return;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+   
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+        {
+        for(int j=0; j<mNumListeners; j++)   
+          EffectMessageSender.newMessage( mListeners.elementAt(j),
+                                          EffectMessage.EFFECT_FINISHED,
+                                          (mID[i]<<Effect.LENGTH)+Effect.FRAGMENT,
+                                          mName[i],
+                                          mObjectID);
+      
+        if( FragmentEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
+          {
+          remove(i);
+          i--;
+          continue;
+          }
+        else mInter[0][i] = null;
+        }
+
+      if( mInter[1][i]!=null ) mInter[2][i].interpolateMain( mUniforms, NUM_UNIFORMS*i+1, mCurrentDuration[i], step);
+      if( mInter[2][i]!=null ) mInter[1][i].interpolateMain( mCache   , NUM_CACHE*i     , mCurrentDuration[i], step);
+      }
+   
+    mTime = currTime;  
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  protected void moveEffect(int index)
+    {
+    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
+    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
+    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
+    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
+
+    mCache[NUM_CACHE*index  ] = mCache[NUM_CACHE*(index+1)  ];
+    mCache[NUM_CACHE*index+1] = mCache[NUM_CACHE*(index+1)+1];
+    mCache[NUM_CACHE*index+2] = mCache[NUM_CACHE*(index+1)+2];
+    mCache[NUM_CACHE*index+3] = mCache[NUM_CACHE*(index+1)+3];
+    }
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  synchronized void send(float halfX, float halfY)
+    {
+    GLES30.glUniform1i( mNumEffectsH, mNumEffects);
+
+    if( mNumEffects>0 )
+      {
+      for(int i=0; i<mNumEffects; i++)
+        {
+        mUniforms[NUM_UNIFORMS*i+4] = mCache[NUM_CACHE*i  ]-halfX;
+        mUniforms[NUM_UNIFORMS*i+5] =-mCache[NUM_CACHE*i+1]+halfY;
+        mUniforms[NUM_UNIFORMS*i+6] = mCache[NUM_CACHE*i+2];
+        mUniforms[NUM_UNIFORMS*i+7] = mCache[NUM_CACHE*i+3];
+        }
+
+      GLES30.glUniform1iv( mTypeH    ,                 mNumEffects, mName    ,0);
+      GLES30.glUniform4fv( mUniformsH,(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
+      }  
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized long add(FragmentEffect fe)
+    {
+    if( mMax[INDEX]>mNumEffects )
+      {
+      int dim0 = 0;
+
+      if( fe.mDynamic0 != null )
+        {
+        mInter[0][mNumEffects] = fe.mDynamic0;
+        dim0 = fe.mDynamic0.getDimension();
+        }
+      else
+        {
+        mInter[0][mNumEffects] = null;
+
+        if( fe.mStatic0 != null )
+          {
+          Static s = fe.mStatic0;
+          dim0 = s.getDimension();
+
+          switch( dim0 )
+            {
+            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
+            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
+            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
+            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
+            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
+            }
+          }
+        }
+
+      if( fe.mDynamic1 != null )
+        {
+        mInter[1][mNumEffects] = fe.mDynamic1;
+        }
+      else
+        {
+        mInter[1][mNumEffects] = null;
+
+        if( fe.mStatic1 != null )
+          {
+          Static s = fe.mStatic1;
+          int dim1 = s.getDimension();
+
+          switch( dim1 )
+            {
+            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 4] = ((Static5D)s).getV();
+            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 3] = ((Static4D)s).getW();
+            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 2] = ((Static3D)s).getZ();
+            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 1] = ((Static2D)s).getY();
+            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0    ] = ((Static1D)s).getX();
+            }
+          }
+        }
+
+      if( fe.mRegion instanceof Dynamic4D)
+        {
+        mInter[2][mNumEffects] = (Dynamic4D)fe.mRegion;
+        }
+      else if( fe.mRegion instanceof Static4D )
+        {
+        mInter[2][mNumEffects]  = null;
+
+        Static4D s = (Static4D)fe.mRegion;
+        mCache[NUM_CACHE*mNumEffects  ] = s.getX();
+        mCache[NUM_CACHE*mNumEffects+1] = s.getY();
+        mCache[NUM_CACHE*mNumEffects+2] = s.getZ();
+        mCache[NUM_CACHE*mNumEffects+3] = s.getW();
+        }
+      else return -1;
+
+      return addBase(fe);
+      }
+      
+    return -1;
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/EffectQueueMatrix.java b/src/main/java/org/distorted/library/main/EffectQueueMatrix.java
new file mode 100644
index 0000000..8aed83d
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/EffectQueueMatrix.java
@@ -0,0 +1,442 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.opengl.GLES30;
+import android.opengl.Matrix;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.MatrixEffect;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.type.Dynamic3D;
+import org.distorted.library.type.Static;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.library.type.Static5D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class EffectQueueMatrix extends EffectQueue
+  {   
+  private static final int NUM_UNIFORMS = 7;
+  private static final int NUM_CACHE    = 0;
+  private static final int INDEX = Effect.MATRIX;
+
+  private static float[] mMVPMatrix = new float[16];
+  private static float[] mTmpMatrix = new float[16];
+  private static float[] mViewMatrix= new float[16];
+
+  private static int mObjDH;      // This is a handle to half a Object dimensions
+  private static int mMVPMatrixH; // the transformation matrix
+  private static int mMVMatrixH;  // the modelview matrix.
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueueMatrix(long id)
+    { 
+    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void multiplyByQuat(float[] matrix, float X, float Y, float Z, float W)
+    {
+    float xx= X * X;
+    float xy= X * Y;
+    float xz= X * Z;
+    float xw= X * W;
+    float yy= Y * Y;
+    float yz= Y * Z;
+    float yw= Y * W;
+    float zz= Z * Z;
+    float zw= Z * W;
+
+    mTmpMatrix[0]  = 1 - 2 * ( yy + zz );
+    mTmpMatrix[1]  =     2 * ( xy - zw );
+    mTmpMatrix[2]  =     2 * ( xz + yw );
+    mTmpMatrix[4]  =     2 * ( xy + zw );
+    mTmpMatrix[5]  = 1 - 2 * ( xx + zz );
+    mTmpMatrix[6]  =     2 * ( yz - xw );
+    mTmpMatrix[8]  =     2 * ( xz - yw );
+    mTmpMatrix[9]  =     2 * ( yz + xw );
+    mTmpMatrix[10] = 1 - 2 * ( xx + yy );
+    mTmpMatrix[3]  = mTmpMatrix[7] = mTmpMatrix[11] = mTmpMatrix[12] = mTmpMatrix[13] = mTmpMatrix[14] = 0;
+    mTmpMatrix[15] = 1;
+    
+    Matrix.multiplyMM(mMVPMatrix, 0, matrix, 0, mTmpMatrix, 0);  
+
+    matrix[ 0] = mMVPMatrix[ 0];
+    matrix[ 1] = mMVPMatrix[ 1];
+    matrix[ 2] = mMVPMatrix[ 2];
+    matrix[ 3] = mMVPMatrix[ 3];
+    matrix[ 4] = mMVPMatrix[ 4];
+    matrix[ 5] = mMVPMatrix[ 5];
+    matrix[ 6] = mMVPMatrix[ 6];
+    matrix[ 7] = mMVPMatrix[ 7];
+    matrix[ 8] = mMVPMatrix[ 8];
+    matrix[ 9] = mMVPMatrix[ 9];
+    matrix[10] = mMVPMatrix[10];
+    matrix[11] = mMVPMatrix[11];
+    matrix[12] = mMVPMatrix[12];
+    matrix[13] = mMVPMatrix[13];
+    matrix[14] = mMVPMatrix[14];
+    matrix[15] = mMVPMatrix[15];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void magnify(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, float marginInPixels)
+    {
+    float scale, nx, ny;
+    float[] result= new float[4];
+    float[] point = new float[4];
+    float[] matrix= new float[16];
+    float minx = Integer.MAX_VALUE;
+    float maxx = Integer.MIN_VALUE;
+    float miny = Integer.MAX_VALUE;
+    float maxy = Integer.MIN_VALUE;
+
+    point[3] = 1.0f;
+
+    Matrix.multiplyMM(matrix, 0, projection.mProjectionMatrix, 0, mViewMatrix, 0);
+
+    point[0] = +halfX; point[1] = +halfY; point[2] = +halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    point[0] = +halfX; point[1] = +halfY; point[2] = -halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    point[0] = +halfX; point[1] = -halfY; point[2] = +halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    point[0] = +halfX; point[1] = -halfY; point[2] = -halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    point[0] = -halfX; point[1] = +halfY; point[2] = +halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    point[0] = -halfX; point[1] = +halfY; point[2] = -halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    point[0] = -halfX; point[1] = -halfY; point[2] = +halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    point[0] = -halfX; point[1] = -halfY; point[2] = -halfZ;
+    Matrix.multiplyMV(result,0,matrix,0,point,0);
+    nx = result[0]/result[3];
+    ny = result[1]/result[3];
+    if( nx<minx ) minx = nx;
+    if( nx>maxx ) maxx = nx;
+    if( ny<miny ) miny = ny;
+    if( ny>maxy ) maxy = ny;
+
+    float xlen = projection.mWidth *(maxx-minx)/2;
+    float ylen = projection.mHeight*(maxy-miny)/2;
+
+    scale = 1.0f + marginInPixels/( xlen>ylen ? ylen:xlen );
+
+    //android.util.Log.d("scale", ""+marginInPixels+" scale= "+scale+" xlen="+xlen+" ylen="+ylen);
+
+    Matrix.scaleM(mViewMatrix, 0, scale, scale, scale);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// here construct the ModelView and the ModelViewProjection Matrices
+
+  private void constructMatrices(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, float marginInPixels)
+    {
+    Matrix.setIdentityM(mViewMatrix, 0);
+    Matrix.translateM(mViewMatrix, 0, -projection.mWidth/2, projection.mHeight/2, -projection.mDistance);
+
+    float x,y,z, sx,sy,sz;
+    float mipmap = projection.mMipmap;
+
+    if( mipmap!=1 ) Matrix.scaleM(mViewMatrix, 0, mipmap, mipmap, mipmap);
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      if (mName[i] == MatrixEffect.ROTATE )
+        {
+        x = mUniforms[NUM_UNIFORMS*i+4];
+        y = mUniforms[NUM_UNIFORMS*i+5];
+        z = mUniforms[NUM_UNIFORMS*i+6];
+
+        Matrix.translateM(mViewMatrix, 0, x,-y, z);
+        Matrix.rotateM( mViewMatrix, 0, mUniforms[NUM_UNIFORMS*i], mUniforms[NUM_UNIFORMS*i+1], mUniforms[NUM_UNIFORMS*i+2], mUniforms[NUM_UNIFORMS*i+3]);
+        Matrix.translateM(mViewMatrix, 0,-x, y,-z);
+        }
+      else if(mName[i] == MatrixEffect.QUATERNION )
+        {
+        x = mUniforms[NUM_UNIFORMS*i+4];
+        y = mUniforms[NUM_UNIFORMS*i+5];
+        z = mUniforms[NUM_UNIFORMS*i+6];
+
+        Matrix.translateM(mViewMatrix, 0, x,-y, z);
+        multiplyByQuat(mViewMatrix, mUniforms[NUM_UNIFORMS*i], mUniforms[NUM_UNIFORMS*i+1], mUniforms[NUM_UNIFORMS*i+2], mUniforms[NUM_UNIFORMS*i+3]);
+        Matrix.translateM(mViewMatrix, 0,-x, y,-z);
+        }
+      else if(mName[i] == MatrixEffect.MOVE )
+        {
+        sx = mUniforms[NUM_UNIFORMS*i  ];
+        sy = mUniforms[NUM_UNIFORMS*i+1];
+        sz = mUniforms[NUM_UNIFORMS*i+2];
+
+        Matrix.translateM(mViewMatrix, 0, sx,-sy, sz);
+        }
+      else if(mName[i] == MatrixEffect.SCALE )
+        {
+        sx = mUniforms[NUM_UNIFORMS*i  ];
+        sy = mUniforms[NUM_UNIFORMS*i+1];
+        sz = mUniforms[NUM_UNIFORMS*i+2];
+
+        Matrix.scaleM(mViewMatrix, 0, sx, sy, sz);
+        }
+      else if(mName[i] == MatrixEffect.SHEAR )
+        {
+        sx = mUniforms[NUM_UNIFORMS*i  ];
+        sy = mUniforms[NUM_UNIFORMS*i+1];
+        sz = mUniforms[NUM_UNIFORMS*i+2];
+
+        x  = mUniforms[NUM_UNIFORMS*i+4];
+        y  = mUniforms[NUM_UNIFORMS*i+5];
+        z  = mUniforms[NUM_UNIFORMS*i+6];
+
+        Matrix.translateM(mViewMatrix, 0, x,-y, z);
+
+        mViewMatrix[4] += sx*mViewMatrix[0]; // Multiply viewMatrix by 1 x 0 0 , i.e. X-shear.
+        mViewMatrix[5] += sx*mViewMatrix[1]; //                        0 1 0 0
+        mViewMatrix[6] += sx*mViewMatrix[2]; //                        0 0 1 0
+        mViewMatrix[7] += sx*mViewMatrix[3]; //                        0 0 0 1
+
+        mViewMatrix[0] += sy*mViewMatrix[4]; // Multiply viewMatrix by 1 0 0 0 , i.e. Y-shear.
+        mViewMatrix[1] += sy*mViewMatrix[5]; //                        y 1 0 0
+        mViewMatrix[2] += sy*mViewMatrix[6]; //                        0 0 1 0
+        mViewMatrix[3] += sy*mViewMatrix[7]; //                        0 0 0 1
+
+        mViewMatrix[4] += sz*mViewMatrix[8]; // Multiply viewMatrix by 1 0 0 0 , i.e. Z-shear.
+        mViewMatrix[5] += sz*mViewMatrix[9]; //                        0 1 0 0
+        mViewMatrix[6] += sz*mViewMatrix[10];//                        0 z 1 0
+        mViewMatrix[7] += sz*mViewMatrix[11];//                        0 0 0 1
+
+        Matrix.translateM(mViewMatrix, 0,-x, y, -z);
+        }
+      }
+
+    Matrix.translateM(mViewMatrix, 0, halfX,-halfY,-halfZ);
+
+    if( marginInPixels!=0 ) magnify(projection,halfX,halfY,halfZ, marginInPixels);
+
+    Matrix.multiplyMM(mMVPMatrix, 0, projection.mProjectionMatrix, 0, mViewMatrix, 0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void getUniforms(int mProgramH)
+    {
+    mObjDH     = GLES30.glGetUniformLocation(mProgramH, "u_objD");
+    mMVPMatrixH= GLES30.glGetUniformLocation(mProgramH, "u_MVPMatrix");
+    mMVMatrixH = GLES30.glGetUniformLocation(mProgramH, "u_MVMatrix"); 
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void compute(long currTime) 
+    {
+    if( currTime==mTime ) return;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+   
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+        {
+        for(int j=0; j<mNumListeners; j++)
+          EffectMessageSender.newMessage( mListeners.elementAt(j),
+                                          EffectMessage.EFFECT_FINISHED,
+                                         (mID[i]<<Effect.LENGTH)+Effect.MATRIX,
+                                          mName[i],
+                                          mObjectID);
+
+        if( MatrixEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
+          {
+          remove(i);
+          i--;
+          continue;
+          }
+        else mInter[0][i] = null;
+        }
+
+      if( mInter[2][i]!=null )
+        {
+        mInter[2][i].interpolateMain(mUniforms, NUM_UNIFORMS*i+4, mCurrentDuration[i], step);
+        }
+      }
+     
+    mTime = currTime;  
+    }  
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  protected void moveEffect(int index)
+    {
+    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
+    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
+    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
+    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
+    mUniforms[NUM_UNIFORMS*index+4] = mUniforms[NUM_UNIFORMS*(index+1)+4];
+    mUniforms[NUM_UNIFORMS*index+5] = mUniforms[NUM_UNIFORMS*(index+1)+5];
+    mUniforms[NUM_UNIFORMS*index+6] = mUniforms[NUM_UNIFORMS*(index+1)+6];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  float[] getMVP()
+    {
+    return mMVPMatrix;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void send(DistortedOutputSurface projection, float halfX, float halfY, float halfZ, float marginInPixels)
+    {
+    constructMatrices(projection,halfX,halfY,halfZ, marginInPixels);
+
+    GLES30.glUniform3f( mObjDH , halfX, halfY, halfZ);
+    GLES30.glUniformMatrix4fv(mMVMatrixH , 1, false, mViewMatrix, 0);
+    GLES30.glUniformMatrix4fv(mMVPMatrixH, 1, false, mMVPMatrix , 0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized long add(MatrixEffect me)
+    {
+    if( mMax[INDEX]>mNumEffects )
+      {
+      int dim0 = 0;
+
+      if( me.mDynamic0 != null )
+        {
+        mInter[0][mNumEffects] = me.mDynamic0;
+        dim0 = me.mDynamic0.getDimension();
+        }
+      else
+        {
+        mInter[0][mNumEffects] = null;
+
+        if( me.mStatic0 != null )
+          {
+          Static s = me.mStatic0;
+          dim0 = s.getDimension();
+
+          switch( dim0 )
+            {
+            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
+            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
+            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
+            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
+            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
+            }
+          }
+        }
+
+      mInter[1][mNumEffects] = null;
+
+      if( me.mStatic1 != null )
+        {
+        Static s = me.mStatic1;
+        int dim1 = s.getDimension();
+
+        switch( dim1 )
+          {
+          case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 4] = ((Static5D)s).getV();
+          case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 3] = ((Static4D)s).getW();
+          case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 2] = ((Static3D)s).getZ();
+          case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 1] = ((Static2D)s).getY();
+          case 1 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0    ] = ((Static1D)s).getX();
+          }
+        }
+
+      if( me.mCenter instanceof Dynamic3D)
+        {
+        mInter[2][mNumEffects] = (Dynamic3D)me.mCenter;
+        }
+      else if( me.mCenter instanceof Static3D )
+        {
+        mInter[2][mNumEffects] = null;
+
+        Static3D s = (Static3D)me.mCenter;
+        mUniforms[NUM_UNIFORMS*mNumEffects+4] = s.getX();
+        mUniforms[NUM_UNIFORMS*mNumEffects+5] = s.getY();
+        mUniforms[NUM_UNIFORMS*mNumEffects+6] = s.getZ();
+        }
+      else return -1;
+
+      return addBase(me);
+      }
+      
+    return -1;
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/EffectQueuePostprocess.java b/src/main/java/org/distorted/library/main/EffectQueuePostprocess.java
new file mode 100644
index 0000000..79109c4
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/EffectQueuePostprocess.java
@@ -0,0 +1,442 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.content.res.Resources;
+import android.opengl.GLES30;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.PostprocessEffect;
+import org.distorted.library.R;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.program.DistortedProgram;
+import org.distorted.library.program.FragmentCompilationException;
+import org.distorted.library.program.FragmentUniformsException;
+import org.distorted.library.program.LinkingException;
+import org.distorted.library.program.VertexCompilationException;
+import org.distorted.library.program.VertexUniformsException;
+import org.distorted.library.type.Static;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.library.type.Static5D;
+
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class EffectQueuePostprocess extends EffectQueue
+  {
+  private static final int MAX_HALO = 50;    // Support effects creating up to MAX_HALO pixels wide 'halo' around the object.
+
+  private static final int POS_DATA_SIZE= 2; // Post Program: size of the position data in elements
+  private static final int TEX_DATA_SIZE= 2; // Post Program: size of the texture coordinate data in elements.
+
+  private static final int NUM_UNIFORMS = 5;
+  private static final int NUM_CACHE    = 0;
+  private static final int INDEX = Effect.POSTPROCESS;
+
+  private static final FloatBuffer mQuadPositions, mQuadTexture, mQuadTextureInv;
+
+  static
+    {
+    int dataLength      = 4;
+    int bytes_per_float = 4;
+
+    float[] position  = { -0.5f, -0.5f,  -0.5f, 0.5f,  0.5f,-0.5f,  0.5f, 0.5f };
+    float[] textureNor= {  0.0f,  0.0f,   0.0f, 1.0f,  1.0f, 0.0f,  1.0f, 1.0f };
+    float[] textureInv= {  0.0f,  0.0f,   1.0f, 0.0f,  0.0f, 1.0f,  1.0f, 1.0f };
+
+    mQuadPositions = ByteBuffer.allocateDirect(POS_DATA_SIZE*dataLength*bytes_per_float).order(ByteOrder.nativeOrder()).asFloatBuffer();
+    mQuadPositions.put(position).position(0);
+    mQuadTexture= ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*bytes_per_float).order(ByteOrder.nativeOrder()).asFloatBuffer();
+    mQuadTexture.put(textureNor).position(0);
+    mQuadTextureInv= ByteBuffer.allocateDirect(TEX_DATA_SIZE*dataLength*bytes_per_float).order(ByteOrder.nativeOrder()).asFloatBuffer();
+    mQuadTextureInv.put(textureInv).position(0);
+    }
+
+  int mQualityLevel;
+  float mQualityScale;
+  private int mHalo;
+
+  /////////////////////////////////////////////////////////////////////////////////
+  // BLUR effect
+  private static final float GAUSSIAN[] =   // G(0.00), G(0.03), G(0.06), ..., G(3.00), 0
+    {                                       // where G(x)= (1/(sqrt(2*PI))) * e^(-(x^2)/2). The last 0 terminates.
+    0.398948f, 0.398769f, 0.398231f, 0.397336f, 0.396086f, 0.394485f, 0.392537f, 0.390247f, 0.387622f, 0.384668f,
+    0.381393f, 0.377806f, 0.373916f, 0.369733f, 0.365268f, 0.360532f, 0.355538f, 0.350297f, 0.344823f, 0.339129f,
+    0.333229f, 0.327138f, 0.320868f, 0.314436f, 0.307856f, 0.301142f, 0.294309f, 0.287373f, 0.280348f, 0.273248f,
+    0.266089f, 0.258884f, 0.251648f, 0.244394f, 0.237135f, 0.229886f, 0.222657f, 0.215461f, 0.208311f, 0.201217f,
+    0.194189f, 0.187238f, 0.180374f, 0.173605f, 0.166940f, 0.160386f, 0.153951f, 0.147641f, 0.141462f, 0.135420f,
+    0.129520f, 0.123765f, 0.118159f, 0.112706f, 0.107408f, 0.102266f, 0.097284f, 0.092461f, 0.087797f, 0.083294f,
+    0.078951f, 0.074767f, 0.070741f, 0.066872f, 0.063158f, 0.059596f, 0.056184f, 0.052920f, 0.049801f, 0.046823f,
+    0.043984f, 0.041280f, 0.038707f, 0.036262f, 0.033941f, 0.031740f, 0.029655f, 0.027682f, 0.025817f, 0.024056f,
+    0.022395f, 0.020830f, 0.019357f, 0.017971f, 0.016670f, 0.015450f, 0.014305f, 0.013234f, 0.012232f, 0.011295f,
+    0.010421f, 0.009606f, 0.008847f, 0.008140f, 0.007483f, 0.006873f, 0.006307f, 0.005782f, 0.005296f, 0.004847f,
+    0.004432f, 0.000000f
+    };
+  private static final int NUM_GAUSSIAN = GAUSSIAN.length-2;
+
+  // The (fixed-function-sampled) Gaussian Blur kernels are of the size k0=1, k1=2, k2=2, k3=3, k4=3, k5=4, k6=4,...
+  // i.e. k(i)=floor((i+3)/2).  (the 'i' in k(i) means 'blur taking into account the present pixel and 'i' pixels
+  // in all 4 directions)
+  // We need room for MAX_BLUR of them, and sum(i=0...N, floor((i+3)/2)) = N + floor(N*N/4)
+  private static float[] weightsCache = new float[MAX_HALO + MAX_HALO*MAX_HALO/4];
+  private static float[] offsetsCache = new float[MAX_HALO + MAX_HALO*MAX_HALO/4];
+
+  private static DistortedProgram mBlur1Program, mBlur2Program;
+  private static int mRadius1H,mOffsets1H,mWeights1H,mDepth1H, mColorTexture1H;
+  private static int mRadius2H,mOffsets2H,mWeights2H,mDepth2H, mColorTexture2H;
+  private static float[] mWeights = new float[MAX_HALO];
+  private static float[] mOffsets = new float[MAX_HALO];
+  /////////////////////////////////////////////////////////////////////////////////
+  // GLOW effect
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  EffectQueuePostprocess(long id)
+    { 
+    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX );
+
+    mQualityLevel = 0;
+    mQualityScale = 1.0f;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void createProgram(Resources resources)
+  throws FragmentCompilationException,VertexCompilationException,VertexUniformsException,FragmentUniformsException,LinkingException
+    {
+    final InputStream blur1VertexStream   = resources.openRawResource(R.raw.blur_vertex_shader);
+    final InputStream blur1FragmentStream = resources.openRawResource(R.raw.blur1_fragment_shader);
+
+    try
+      {
+      mBlur1Program = new DistortedProgram(blur1VertexStream,blur1FragmentStream,
+                                          Distorted.GLSL_VERSION,
+                                          Distorted.GLSL_VERSION + "#define MAX_BLUR "+MAX_HALO, Distorted.GLSL);
+      }
+    catch(Exception e)
+      {
+      android.util.Log.e("EFFECTS", "exception trying to compile BLUR1 program: "+e.getMessage());
+      throw new RuntimeException(e.getMessage());
+      }
+
+    int blur1ProgramH = mBlur1Program.getProgramHandle();
+    mRadius1H       = GLES30.glGetUniformLocation( blur1ProgramH, "u_Radius");
+    mOffsets1H      = GLES30.glGetUniformLocation( blur1ProgramH, "u_Offsets");
+    mWeights1H      = GLES30.glGetUniformLocation( blur1ProgramH, "u_Weights");
+    mDepth1H        = GLES30.glGetUniformLocation( blur1ProgramH, "u_Depth");
+    mColorTexture1H = GLES30.glGetUniformLocation( blur1ProgramH, "u_ColorTexture");
+
+    final InputStream blur2VertexStream   = resources.openRawResource(R.raw.blur_vertex_shader);
+    final InputStream blur2FragmentStream = resources.openRawResource(R.raw.blur2_fragment_shader);
+
+    try
+      {
+      mBlur2Program = new DistortedProgram(blur2VertexStream,blur2FragmentStream,
+                                          Distorted.GLSL_VERSION,
+                                          Distorted.GLSL_VERSION + "#define MAX_BLUR "+MAX_HALO, Distorted.GLSL);
+      }
+    catch(Exception e)
+      {
+      android.util.Log.e("EFFECTS", "exception trying to compile BLUR2 program: "+e.getMessage());
+      // run anyway as compiling Blur2 WILL fail on OpenGL 2.0 contexts
+      mBlur2Program = mBlur1Program;
+      }
+
+    int blur2ProgramH = mBlur2Program.getProgramHandle();
+    mRadius2H       = GLES30.glGetUniformLocation( blur2ProgramH, "u_Radius");
+    mOffsets2H      = GLES30.glGetUniformLocation( blur2ProgramH, "u_Offsets");
+    mWeights2H      = GLES30.glGetUniformLocation( blur2ProgramH, "u_Weights");
+    mDepth2H        = GLES30.glGetUniformLocation( blur2ProgramH, "u_Depth");
+    mColorTexture2H = GLES30.glGetUniformLocation( blur2ProgramH, "u_ColorTexture");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean compute(long currTime)
+    {
+    if( currTime==mTime ) return false;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+
+    mHalo = 0;
+    int halo;
+
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      if( mInter[0][i]!=null && mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+        {
+        for(int j=0; j<mNumListeners; j++)
+          EffectMessageSender.newMessage( mListeners.elementAt(j),
+                                          EffectMessage.EFFECT_FINISHED,
+                                         (mID[i]<<Effect.LENGTH)+Effect.POSTPROCESS,
+                                          mName[i],
+                                          mObjectID);
+
+        if( PostprocessEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
+          {
+          remove(i);
+          i--;
+          continue;
+          }
+        else mInter[0][i] = null;
+        }
+
+      if( mInter[1][i]!=null ) mInter[1][i].interpolateMain( mUniforms, NUM_UNIFORMS*i+1, mCurrentDuration[i], step);
+
+      halo = (int)mUniforms[NUM_UNIFORMS*i];
+      if( halo>mHalo ) mHalo = halo;
+      }
+
+    mTime = currTime;
+
+    return true;
+    }  
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  protected void moveEffect(int index)
+    {
+    mUniforms[NUM_UNIFORMS*index  ] = mUniforms[NUM_UNIFORMS*(index+1)  ];
+    mUniforms[NUM_UNIFORMS*index+1] = mUniforms[NUM_UNIFORMS*(index+1)+1];
+    mUniforms[NUM_UNIFORMS*index+2] = mUniforms[NUM_UNIFORMS*(index+1)+2];
+    mUniforms[NUM_UNIFORMS*index+3] = mUniforms[NUM_UNIFORMS*(index+1)+3];
+    mUniforms[NUM_UNIFORMS*index+4] = mUniforms[NUM_UNIFORMS*(index+1)+4];
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// This implements the 'Better separable implementation using GPU fixed function sampling' from
+// https://software.intel.com/en-us/blogs/2014/07/15/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms
+
+  private void computeGaussianKernel(int radius)
+    {
+    int offset = radius + radius*radius/4;
+
+    if( weightsCache[offset]==0.0f )
+      {
+      float z, x= 0.0f, P= (float)NUM_GAUSSIAN / (radius>3 ? radius:3);
+      mWeights[0] = GAUSSIAN[0];
+      float sum   = GAUSSIAN[0];
+      int j;
+
+      for(int i=1; i<=radius; i++)
+        {
+        x += P;
+        j = (int)x;
+        z = x-j;
+
+        mWeights[i] = (1-z)*GAUSSIAN[j] + z*GAUSSIAN[j+1];
+        sum += 2*mWeights[i];
+        }
+
+      for(int i=0; i<=radius; i++) mWeights[i] /= sum;
+
+      int numloops = radius/2;
+      weightsCache[offset] = mWeights[0];
+      offsetsCache[offset] = 0.0f;
+
+      for(int i=0; i<numloops; i++)
+        {
+        offsetsCache[offset+i+1] = mWeights[2*i+1]*(2*i+1) + mWeights[2*i+2]*(2*i+2);
+        weightsCache[offset+i+1] = mWeights[2*i+1] + mWeights[2*i+2];
+        offsetsCache[offset+i+1]/= weightsCache[offset+i+1];
+        }
+
+      if( radius%2 == 1 )
+        {
+        int index = offset + radius/2 +1;
+        offsetsCache[index]=radius;
+        weightsCache[index]=mWeights[radius];
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getHalo()
+    {
+    return mNumEffects>0 ? mHalo : 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int postprocess(long time, DistortedOutputSurface surface)
+    {
+    int numRenders = 0;
+
+    if( mNumEffects>0 )
+      {
+      compute(time);
+
+      for(int i=0; i<mNumEffects; i++)
+        {
+        if (mName[i] == PostprocessEffect.BLUR )
+          {
+          blur(NUM_UNIFORMS*i,surface);
+          numRenders += 2;
+          }
+        else if (mName[i] == PostprocessEffect.GLOW )
+          {
+          glow(NUM_UNIFORMS*i,surface);
+          numRenders += 2;
+          }
+        }
+      }
+
+    return numRenders;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void blur(int index, DistortedOutputSurface surface)
+    {
+    DistortedFramebuffer buffer = surface.mBuffer[mQualityLevel];
+    GLES30.glBindFramebuffer(GLES30.GL_FRAMEBUFFER, buffer.mFBOH[0]);
+
+    float w1 = buffer.mWidth;
+    float h1 = buffer.mHeight;
+
+    int radius = (int)(mUniforms[index]*mQualityScale);
+    if( radius>=MAX_HALO ) radius = MAX_HALO-1;
+    computeGaussianKernel(radius);
+
+    int offset = radius + radius*radius/4;
+    radius = (radius+1)/2;
+
+    // horizontal blur
+    GLES30.glViewport(0, 0, (int)w1, (int)h1);
+    mBlur1Program.useProgram();
+    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[1], 0);
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mColorH[0]);
+
+    GLES30.glUniform1fv( mWeights1H, radius+1, weightsCache,offset);
+    GLES30.glUniform1i( mRadius1H, radius);
+    GLES30.glUniform1f( mDepth1H , 1.0f-surface.mNear);
+    GLES30.glUniform1i( mColorTexture1H , 0 );
+    for(int i=0; i<=radius; i++) mOffsets[i] = offsetsCache[offset+i]/h1;
+    GLES30.glUniform1fv( mOffsets1H ,radius+1, mOffsets,0);
+    GLES30.glVertexAttribPointer(mBlur1Program.mAttribute[0], POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadPositions);
+    GLES30.glVertexAttribPointer(mBlur1Program.mAttribute[1], TEX_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadTexture);
+
+    DistortedRenderState.useStencilMark();
+    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
+    DistortedRenderState.unuseStencilMark();
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
+
+    // vertical blur
+    mBlur2Program.useProgram();
+    GLES30.glFramebufferTexture2D(GLES30.GL_FRAMEBUFFER, GLES30.GL_COLOR_ATTACHMENT0, GLES30.GL_TEXTURE_2D, buffer.mColorH[0], 0);
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, buffer.mColorH[1]);
+
+    GLES30.glColorMask(true,true,true,true);
+    GLES30.glClearColor(0.0f,0.0f,0.0f,0.0f);
+    GLES30.glClear(GLES30.GL_COLOR_BUFFER_BIT);
+
+    GLES30.glUniform1fv( mWeights2H, radius+1, weightsCache,offset);
+    GLES30.glUniform1i( mRadius2H, radius);
+    GLES30.glUniform1f( mDepth2H , 1.0f-surface.mNear);
+    GLES30.glUniform1i( mColorTexture2H , 0 );
+    for(int i=0; i<=radius; i++) mOffsets[i] = offsetsCache[offset+i]/w1;
+    GLES30.glUniform1fv( mOffsets2H ,radius+1, mOffsets,0);
+    GLES30.glVertexAttribPointer(mBlur2Program.mAttribute[0], POS_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadPositions);
+    GLES30.glVertexAttribPointer(mBlur2Program.mAttribute[1], TEX_DATA_SIZE, GLES30.GL_FLOAT, false, 0, mQuadTexture);
+
+    DistortedRenderState.useStencilMark();
+    GLES30.glDrawArrays(GLES30.GL_TRIANGLE_STRIP, 0, 4);
+    DistortedRenderState.unuseStencilMark();
+    GLES30.glActiveTexture(GLES30.GL_TEXTURE0);
+    GLES30.glBindTexture(GLES30.GL_TEXTURE_2D, 0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void glow(int index, DistortedOutputSurface surface)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized long add(PostprocessEffect pe)
+    {
+    if( mMax[INDEX]>mNumEffects )
+      {
+      int dim0 = 0;
+
+      if( pe.mDynamic0 != null )
+        {
+        mInter[0][mNumEffects] = pe.mDynamic0;
+        dim0 = pe.mDynamic0.getDimension();
+        }
+      else
+        {
+        mInter[0][mNumEffects] = null;
+
+        if( pe.mStatic0 != null )
+          {
+          Static s = pe.mStatic0;
+          dim0 = s.getDimension();
+
+          switch( dim0 )
+            {
+            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
+            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
+            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
+            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
+            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
+            }
+          }
+        }
+
+      mInter[1][mNumEffects] = null;
+
+      if( pe.mStatic1 != null )
+        {
+        Static s = pe.mStatic1;
+        int dim1 = s.getDimension();
+
+        switch( dim1 )
+          {
+          case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 4] = ((Static5D)s).getV();
+          case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 3] = ((Static4D)s).getW();
+          case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 2] = ((Static3D)s).getZ();
+          case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0 + 1] = ((Static2D)s).getY();
+          case 1 : mUniforms[NUM_UNIFORMS*mNumEffects + dim0    ] = ((Static1D)s).getX();
+          }
+        }
+
+      return addBase(pe);
+      }
+
+    return -1;
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/EffectQueueVertex.java b/src/main/java/org/distorted/library/main/EffectQueueVertex.java
new file mode 100644
index 0000000..a56a53a
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/EffectQueueVertex.java
@@ -0,0 +1,257 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.opengl.GLES30;
+
+import org.distorted.library.effect.Effect;
+import org.distorted.library.effect.VertexEffect;
+import org.distorted.library.message.EffectMessage;
+import org.distorted.library.type.Dynamic3D;
+import org.distorted.library.type.Dynamic4D;
+import org.distorted.library.type.Static;
+import org.distorted.library.type.Static1D;
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static3D;
+import org.distorted.library.type.Static4D;
+import org.distorted.library.type.Static5D;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+class EffectQueueVertex extends EffectQueue
+  { 
+  private static final int NUM_UNIFORMS = 12;
+  private static final int NUM_CACHE    =  3;
+  private static final int INDEX = Effect.VERTEX;
+  private static int mNumEffectsH;
+  private static int mTypeH;
+  private static int mUniformsH;
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  EffectQueueVertex(long id)
+    { 
+    super(id,NUM_UNIFORMS,NUM_CACHE,INDEX);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  static void getUniforms(int mProgramH)
+    {
+    mNumEffectsH= GLES30.glGetUniformLocation( mProgramH, "vNumEffects");
+    mTypeH      = GLES30.glGetUniformLocation( mProgramH, "vType");
+    mUniformsH  = GLES30.glGetUniformLocation( mProgramH, "vUniforms");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+  
+  synchronized void compute(long currTime) 
+    {
+    if( currTime==mTime ) return;
+    if( mTime==0 ) mTime = currTime;
+    long step = (currTime-mTime);
+   
+    for(int i=0; i<mNumEffects; i++)
+      {
+      mCurrentDuration[i] += step;
+
+      if( mInter[0][i]!=null )
+        {
+        if( mInter[0][i].interpolateMain(mUniforms ,NUM_UNIFORMS*i, mCurrentDuration[i], step) )
+          {
+          postprocess(i);
+
+          for(int j=0; j<mNumListeners; j++)
+            EffectMessageSender.newMessage( mListeners.elementAt(j),
+                                            EffectMessage.EFFECT_FINISHED,
+                                           (mID[i]<<Effect.LENGTH)+Effect.VERTEX,
+                                            mName[i],
+                                            mObjectID);
+
+          if( VertexEffect.isUnity(mName[i], mUniforms, NUM_UNIFORMS*i) )
+            {
+            remove(i);
+            i--;
+            continue;
+            }
+          else mInter[0][i] = null;
+          }
+        else
+          {
+          postprocess(i);
+          }
+        }
+
+      if( mInter[1][i]!=null ) mInter[1][i].interpolateMain(mUniforms, NUM_UNIFORMS*i+8, mCurrentDuration[i], step);
+      if( mInter[2][i]!=null ) mInter[2][i].interpolateMain(mCache   , NUM_CACHE*i     , mCurrentDuration[i], step);
+      }
+     
+    mTime = currTime;  
+    }  
+  
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  protected void moveEffect(int index)
+    {
+    mUniforms[NUM_UNIFORMS*index   ] = mUniforms[NUM_UNIFORMS*(index+1)   ];
+    mUniforms[NUM_UNIFORMS*index+ 1] = mUniforms[NUM_UNIFORMS*(index+1)+ 1];
+    mUniforms[NUM_UNIFORMS*index+ 2] = mUniforms[NUM_UNIFORMS*(index+1)+ 2];
+    mUniforms[NUM_UNIFORMS*index+ 3] = mUniforms[NUM_UNIFORMS*(index+1)+ 3];
+    mUniforms[NUM_UNIFORMS*index+ 4] = mUniforms[NUM_UNIFORMS*(index+1)+ 4];
+
+    mCache[NUM_CACHE*index  ] = mCache[NUM_CACHE*(index+1)  ];
+    mCache[NUM_CACHE*index+1] = mCache[NUM_CACHE*(index+1)+1];
+    mCache[NUM_CACHE*index+2] = mCache[NUM_CACHE*(index+1)+2];
+
+    mUniforms[NUM_UNIFORMS*index+ 8] = mUniforms[NUM_UNIFORMS*(index+1)+ 8];
+    mUniforms[NUM_UNIFORMS*index+ 9] = mUniforms[NUM_UNIFORMS*(index+1)+ 9];
+    mUniforms[NUM_UNIFORMS*index+10] = mUniforms[NUM_UNIFORMS*(index+1)+10];
+    mUniforms[NUM_UNIFORMS*index+11] = mUniforms[NUM_UNIFORMS*(index+1)+11];
+    }
+   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void send(float halfX, float halfY, float halfZ)
+    {
+    GLES30.glUniform1i( mNumEffectsH, mNumEffects);
+      
+    if( mNumEffects>0 )
+      {
+      for(int i=0; i<mNumEffects; i++)
+        {
+        mUniforms[NUM_UNIFORMS*i+5] = mCache[NUM_CACHE*i  ]-halfX;
+        mUniforms[NUM_UNIFORMS*i+6] =-mCache[NUM_CACHE*i+1]+halfY;
+        mUniforms[NUM_UNIFORMS*i+7] = mCache[NUM_CACHE*i+2]-halfZ;
+        }
+
+      GLES30.glUniform1iv( mTypeH    ,                 mNumEffects, mName    ,0);
+      GLES30.glUniform4fv( mUniformsH,(NUM_UNIFORMS/4)*mNumEffects, mUniforms,0);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Do various post-processing on already computed effects.
+// 1) here unlike in the fragment queue, we don't have to multiply the points by ModelView matrix because that gets done in the shader.
+// 2) in case of SWIRL, switch the angles from degrees to radians
+// 3) likewise in case of WAVE and PINCH
+// 4) In case of DISTORT, invert the Y-axis
+  
+  private void postprocess(int effect)
+    {
+    if( mName[effect]==VertexEffect.SWIRL )
+      {
+      mUniforms[NUM_UNIFORMS*effect  ] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect  ]/180);
+      }
+    if( mName[effect]==VertexEffect.PINCH )
+      {
+      mUniforms[NUM_UNIFORMS*effect+1] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+1]/180);
+      }
+    if( mName[effect]==VertexEffect.WAVE )
+      {
+      mUniforms[NUM_UNIFORMS*effect+2] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+2]/180);
+      mUniforms[NUM_UNIFORMS*effect+3] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+3]/180);
+      mUniforms[NUM_UNIFORMS*effect+4] = (float)(Math.PI*mUniforms[NUM_UNIFORMS*effect+4]/180);
+      }
+    if( mName[effect]==VertexEffect.DISTORT )
+      {
+      mUniforms[NUM_UNIFORMS*effect+1] =-mUniforms[NUM_UNIFORMS*effect+1];
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized long add(VertexEffect ve)
+    {
+    if( mMax[INDEX]>mNumEffects )
+      {
+      if( ve.mDynamic0 != null )
+        {
+        mInter[0][mNumEffects] = ve.mDynamic0;
+        }
+      else
+        {
+        mInter[0][mNumEffects] = null;
+
+        if( ve.mStatic0 != null )
+          {
+          Static s = ve.mStatic0;
+
+          switch( s.getDimension() )
+            {
+            case 5 : mUniforms[NUM_UNIFORMS*mNumEffects + 4] = ((Static5D)s).getV();
+            case 4 : mUniforms[NUM_UNIFORMS*mNumEffects + 3] = ((Static4D)s).getW();
+            case 3 : mUniforms[NUM_UNIFORMS*mNumEffects + 2] = ((Static3D)s).getZ();
+            case 2 : mUniforms[NUM_UNIFORMS*mNumEffects + 1] = ((Static2D)s).getY();
+            case 1 : mUniforms[NUM_UNIFORMS*mNumEffects    ] = ((Static1D)s).getX();
+            }
+          }
+        }
+
+      if( ve.mRegion!=null )
+        {
+        if( ve.mRegion instanceof Dynamic4D)
+          {
+          mInter[1][mNumEffects] = (Dynamic4D)ve.mRegion;
+          }
+        else if ( ve.mRegion instanceof Static4D)
+          {
+          Static4D tmp = (Static4D)ve.mRegion;
+
+          float z = tmp.getZ();
+
+          mUniforms[NUM_UNIFORMS*mNumEffects+ 8] = tmp.getX();
+          mUniforms[NUM_UNIFORMS*mNumEffects+ 9] =-tmp.getY();   // invert y already
+          mUniforms[NUM_UNIFORMS*mNumEffects+10] = z<=0.0f ? Float.MAX_VALUE : z;
+          mUniforms[NUM_UNIFORMS*mNumEffects+11] = tmp.getW();
+          mInter[1][mNumEffects] = null;
+          }
+        else return -1;
+        }
+      else
+        {
+        mUniforms[NUM_UNIFORMS*mNumEffects+ 8] = 0.0f;
+        mUniforms[NUM_UNIFORMS*mNumEffects+ 9] = 0.0f;
+        mUniforms[NUM_UNIFORMS*mNumEffects+10] = Float.MAX_VALUE;
+        mUniforms[NUM_UNIFORMS*mNumEffects+11] = 0.0f;
+        mInter[1][mNumEffects] = null;
+        }
+
+      if( ve.mCenter instanceof Dynamic3D)
+        {
+        mInter[2][mNumEffects] = (Dynamic3D)ve.mCenter;
+        }
+      else if( ve.mCenter instanceof Static3D)
+        {
+        mInter[2][mNumEffects] = null;
+        mCache[NUM_CACHE*mNumEffects  ] = ((Static3D)ve.mCenter).getX();
+        mCache[NUM_CACHE*mNumEffects+1] = ((Static3D)ve.mCenter).getY();
+        mCache[NUM_CACHE*mNumEffects+2] = ((Static3D)ve.mCenter).getZ();
+        }
+
+      long ret= addBase(ve);
+
+      postprocess(mNumEffects-1); //addBase just incremented mNumEffects
+
+      return ret;
+      }
+
+    return -1;
+    }
+  }
diff --git a/src/main/java/org/distorted/library/main/MeshCubes.java b/src/main/java/org/distorted/library/main/MeshCubes.java
new file mode 100644
index 0000000..ab36813
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/MeshCubes.java
@@ -0,0 +1,764 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create a 3D grid composed of Cubes.
+ * <p>
+ * Any subset of a MxNx1 cuboid is possible.
+ */
+public class MeshCubes extends MeshObject
+   {
+   private static final float R = 0.0f;//0.2f;
+
+   private static final int NORTH = 0;
+   private static final int WEST  = 1;
+   private static final int EAST  = 2;
+   private static final int SOUTH = 3;
+
+   private static final float[] mNormalX = new float[4];
+   private static final float[] mNormalY = new float[4];
+   private static final float[] mNormalZ = new float[4];
+
+   private class Edge
+     {
+     final int side; 
+     final int row;
+     final int col;
+     
+     Edge(int s, int r, int c)
+       {
+       side= s; 
+       row = r;
+       col = c;
+       }
+     }
+   
+   private int mCols, mRows, mSlices;
+   private int[][] mCubes;
+   private ArrayList<Edge> mEdges = new ArrayList<>();
+
+   private int remainingVert;
+   private int mSideBends;
+   private int mEdgeNum;
+   private int mSideWalls;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// a Block is split into two triangles along the NE-SW line iff it is in the top-right
+// or bottom-left quadrant of the grid.
+
+   private boolean isNE(int row,int col)
+     {
+     return ( (2*row<mRows)^(2*col<mCols) );
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// return the number of vertices our grid will contain
+
+   private int computeDataLength()
+      {
+      int frontWalls=0, frontSegments=0, triangleShifts=0, windingShifts=0;
+      int shiftCol = (mCols-1)/2;
+
+      boolean lastBlockIsNE=false;
+      boolean thisBlockIsNE;        // the block we are currently looking at is split into
+                                    // two triangles along the NE-SW line (rather than NW-SE)
+      for(int row=0; row<mRows; row++)
+        {
+        if( mCols>=2 && (mCubes[row][shiftCol]%2 == 1) && (mCubes[row][shiftCol+1]%2 == 1) ) triangleShifts++;
+
+        for(int col=0; col<mCols; col++)
+          {
+          if( mCubes[row][col]%2 == 1 )  // land
+            {
+            thisBlockIsNE = isNE(row,col);
+            if( thisBlockIsNE^lastBlockIsNE ) windingShifts++;
+            lastBlockIsNE = thisBlockIsNE;
+            frontWalls++;
+            if( col==mCols-1 || mCubes[row][col+1]%2 == 0 ) frontSegments++;
+            }
+          }
+        }
+
+      int frontVert       = 2*( frontWalls + 2*frontSegments - 1) +2*triangleShifts + windingShifts;
+      int sideVertOneSlice= 2*( mSideWalls + mSideBends + mEdgeNum -1);
+      int sideVert        = 2*(mSlices-1) + mSlices*sideVertOneSlice;
+      int firstWinding    = (mSlices>0 && (frontVert+1)%2==1 ) ? 1:0;
+      int dataL           = mSlices==0 ? frontVert : (frontVert+1) +firstWinding+ (1+sideVert+1) + (1+frontVert);
+/*
+      android.util.Log.e("CUBES","triangleShifts="+triangleShifts+" windingShifts="+windingShifts+" winding1="+firstWinding+" frontVert="+frontVert+" sideVert="+sideVert);
+      android.util.Log.e("CUBES", "frontW="+frontWalls+" fSegments="+frontSegments+" sWalls="+mSideWalls+" sSegments="+mEdgeNum+" sideBends="+mSideBends+" dataLen="+dataL );
+*/
+      return dataL<0 ? 0:dataL;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+   private static String debug(short[] val)
+     {
+     String ret="";j
+     
+     for(int i=0; i<val.length; i++) ret+=(" "+val[i]); 
+     
+     return ret;
+     }
+*/
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+   private static String debug(float[] val, int stop)
+     {
+     String ret="";
+
+     for(int i=0; i<val.length; i++) 
+        {
+        if( i%stop==0 ) ret+="\n";
+        ret+=(" "+val[i]);
+        }
+
+     return ret;
+     }
+*/
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+   private static String debug(Edge e)
+     {
+     String d = "";
+     
+     switch(e.side)
+       {
+       case NORTH: d+="NORTH "; break;
+       case SOUTH: d+="SOUTH "; break;
+       case WEST : d+="WEST  "; break;
+       case EAST : d+="EAST  "; break;
+       }
+     
+     d+=("("+e.row+","+e.col+")");
+     
+     return d;
+     }   
+*/
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   private void prepareDataStructures(int cols, String desc, int slices)
+     {
+     mRows      =0;
+     mCols      =0;
+     mSlices    =slices;
+     numVertices=0;
+     
+     if( cols>0 && desc.contains("1") )
+       {
+       mCols = cols;
+       mRows = desc.length()/cols;
+
+       mCubes = new int[mRows][mCols];
+       
+       for(int j=0; j<mCols; j++)
+         for(int i=0; i<mRows; i++)
+           mCubes[i][j] = (desc.charAt(i*mCols+j) == '1' ? 1:0);
+
+       markRegions();
+       numVertices = computeDataLength();
+
+       remainingVert = numVertices;
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// full grid
+
+   private void prepareDataStructures(int cols, int rows, int slices)
+     {
+     mRows       =rows;
+     mCols       =cols;
+     mSlices     =slices;
+     numVertices =   0;
+
+     if( cols>0 && rows>0 )
+       {
+       mCubes = new int[mRows][mCols];
+
+       for(int j=0; j<mCols; j++)
+         for(int i=0; i<mRows; i++)
+           mCubes[i][j] = 1;
+
+       markRegions();
+       numVertices = computeDataLength();
+
+       remainingVert = numVertices;
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Mark all the 'regions' of our grid  - i.e. separate pieces of 'land' (connected blocks that will 
+// be rendered) and 'water' (connected holes in between) with integers. Each connected block of land
+// gets a unique odd integer, each connected block of water a unique even integer.
+//
+// Water on the edges of the grid is also considered connected to itself!   
+//   
+// This function also creates a list of 'Edges'. Each Edge is a data structure from which later on we
+// will start building the side walls of each connected block of land (and sides of holes of water
+// inside). Each Edge needs to point from Land to Water (thus the '(SOUTH,i-1,j)' below) - otherwise
+// later on setting up normal vectors wouldn't work.
+   
+  private void markRegions()
+     {
+     int i, j, numWater=1, numLand=0;
+     
+     for(i=0; i<mRows;i++) if( mCubes[      i][      0]==0 ) markRegion((short)2,      i,       0);
+     for(i=0; i<mRows;i++) if( mCubes[      i][mCols-1]==0 ) markRegion((short)2,      i, mCols-1);
+     for(i=0; i<mCols;i++) if( mCubes[0      ][      i]==0 ) markRegion((short)2,      0,       i);
+     for(i=0; i<mCols;i++) if( mCubes[mRows-1][      i]==0 ) markRegion((short)2,mRows-1,       i);
+           
+     for(i=0; i<mRows; i++)
+        for(j=0; j<mCols; j++)
+           {
+           if( mCubes[i][j] == 0 ) { numWater++; markRegion( (short)(2*numWater ),i,j); mEdges.add(new Edge(SOUTH,i-1,j)); }
+           if( mCubes[i][j] == 1 ) { numLand ++; markRegion( (short)(2*numLand+1),i,j); mEdges.add(new Edge(NORTH,i  ,j)); }
+           }
+     
+     // now we potentially need to kick out some Edges . Otherwise the following does not work:
+     //
+     // 0 1 0
+     // 1 0 1
+     // 0 1 0
+     
+     mEdgeNum= mEdges.size();
+     int initCol, initRow, initSide, lastSide;
+     Edge e1,e2;
+     
+     for(i=0; i<mEdgeNum; i++)
+       {
+       e1 = mEdges.get(i);
+       initRow= e1.row;
+       initCol= e1.col;
+       initSide=e1.side;
+
+       do
+         {
+         //android.util.Log.d("CUBES", "checking edge "+debug(e1));
+
+         mSideWalls++;
+
+         if( e1.side==NORTH || e1.side==SOUTH )
+           {
+           for(j=i+1;j<mEdgeNum;j++)
+             {
+             e2 = mEdges.get(j);
+
+             if( e2.side==e1.side && e2.row==e1.row && e2.col==e1.col )
+               {
+               mEdges.remove(j);
+               mEdgeNum--;
+               j--;
+
+               //android.util.Log.e("CUBES", "removing edge "+debug(e2));
+               }
+             }
+           }
+
+         lastSide = e1.side;
+         e1 = getNextEdge(e1);
+         if( e1.side!=lastSide ) mSideBends++;
+         }
+       while( e1.col!=initCol || e1.row!=initRow || e1.side!=initSide );
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// when calling, make sure that newVal != val
+   
+  private void markRegion(short newVal, int row, int col)
+     {
+     int val = mCubes[row][col];
+     mCubes[row][col] = newVal;
+     
+     if( row>0       && mCubes[row-1][col  ]==val ) markRegion(newVal, row-1, col  );
+     if( row<mRows-1 && mCubes[row+1][col  ]==val ) markRegion(newVal, row+1, col  );
+     if( col>0       && mCubes[row  ][col-1]==val ) markRegion(newVal, row  , col-1);
+     if( col<mCols-1 && mCubes[row  ][col+1]==val ) markRegion(newVal, row  , col+1);
+     }
+   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  private void createNormals(boolean front, int row, int col)
+     {
+     int td,lr; 
+      
+     int nw = (col>0       && row>0      ) ? (mCubes[row-1][col-1]%2) : 0;
+     int w  = (col>0                     ) ? (mCubes[row  ][col-1]%2) : 0;
+     int n  = (               row>0      ) ? (mCubes[row-1][col  ]%2) : 0;
+     int c  =                                (mCubes[row  ][col  ]%2);
+     int sw = (col>0       && row<mRows-1) ? (mCubes[row+1][col-1]%2) : 0;
+     int s  = (               row<mRows-1) ? (mCubes[row+1][col  ]%2) : 0;
+     int ne = (col<mCols-1 && row>0      ) ? (mCubes[row-1][col+1]%2) : 0;
+     int e  = (col<mCols-1               ) ? (mCubes[row  ][col+1]%2) : 0;
+     int se = (col<mCols-1 && row<mRows-1) ? (mCubes[row+1][col+1]%2) : 0;
+
+     if(front)
+       {
+       mNormalZ[0] = 1.0f;
+       mNormalZ[1] = 1.0f;
+       mNormalZ[2] = 1.0f;
+       mNormalZ[3] = 1.0f;
+       }
+     else
+       {
+       mNormalZ[0] =-1.0f;
+       mNormalZ[1] =-1.0f;
+       mNormalZ[2] =-1.0f;
+       mNormalZ[3] =-1.0f;
+       }
+
+     td = nw+n-w-c;
+     lr = c+n-w-nw;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[0] = lr*R;
+     mNormalY[0] = td*R;
+     
+     td = w+c-sw-s;
+     lr = c+s-w-sw;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[1] = lr*R;
+     mNormalY[1] = td*R;
+     
+     td = n+ne-c-e;
+     lr = e+ne-c-n;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[2] = lr*R;
+     mNormalY[2] = td*R;
+     
+     td = c+e-s-se;
+     lr = e+se-c-s;
+     if( td<0 ) td=-1;
+     if( td>0 ) td= 1;
+     if( lr<0 ) lr=-1;
+     if( lr>0 ) lr= 1;
+     mNormalX[3] = lr*R;
+     mNormalY[3] = td*R;
+     /*
+     android.util.Log.d("CUBES", "row="+row+" col="+col);
+     android.util.Log.d("CUBES", mNormalX[0]+" "+mNormalY[0]);
+     android.util.Log.d("CUBES", mNormalX[1]+" "+mNormalY[1]);
+     android.util.Log.d("CUBES", mNormalX[2]+" "+mNormalY[2]);
+     android.util.Log.d("CUBES", mNormalX[3]+" "+mNormalY[3]);
+     */
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int buildFrontBackGrid(boolean front, int vertex, float[] attribs)
+     {
+     int last, current;
+     boolean seenLand=false;
+     boolean lastBlockIsNE = false;
+     boolean currentBlockIsNE;
+     float vectZ = (front ? 0.5f : -0.5f);
+
+     //android.util.Log.d("CUBES", "buildFrontBack");
+
+     for(int row=0; row<mRows; row++)
+       {
+       last =0;
+         
+       for(int col=0; col<mCols; col++)
+         {
+         current = mCubes[row][col];
+
+         if( current%2 == 1 )
+           {
+           currentBlockIsNE = isNE(row,col);
+
+           if( !seenLand && !front && ((vertex%2==1)^currentBlockIsNE) )
+             {
+             //android.util.Log.d("CUBES","repeating winding2 vertex");
+
+             vertex = repeatLast(vertex,attribs);
+             }
+
+           createNormals(front,row,col);
+
+           if( currentBlockIsNE )
+             {
+             if( (last!=current) || !lastBlockIsNE )
+               {
+               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
+               vertex= addFrontVertex( vertex, 0, vectZ, col, row, attribs);
+               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
+               if( !lastBlockIsNE || (!front && !seenLand) ) vertex = repeatLast(vertex,attribs);
+               vertex= addFrontVertex( vertex, 1, vectZ, col, row+1, attribs);
+               }
+             vertex= addFrontVertex( vertex, 2, vectZ, col+1, row, attribs);
+             vertex= addFrontVertex( vertex, 3, vectZ, col+1, row+1, attribs);
+             }
+           else
+             {
+             if( (last!=current) || lastBlockIsNE )
+               {
+               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
+               vertex= addFrontVertex( vertex, 1, vectZ, col, row+1, attribs);
+               if( seenLand  && (last != current) ) vertex = repeatLast(vertex,attribs);
+               if( lastBlockIsNE || (!front && !seenLand) ) vertex = repeatLast(vertex,attribs);
+               vertex= addFrontVertex( vertex, 0, vectZ, col, row, attribs);
+               }
+             vertex= addFrontVertex( vertex, 3, vectZ, col+1, row+1, attribs);
+             vertex= addFrontVertex( vertex, 2, vectZ, col+1, row  , attribs);
+             }
+
+           seenLand = true;
+           lastBlockIsNE = currentBlockIsNE;
+           }
+            
+         last = current;
+         }
+       }
+     
+     return vertex;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int repeatLast(int vertex, float[] attribs)
+     {
+     //android.util.Log.e("CUBES", "repeating last vertex!");
+
+     if( vertex>0 )
+       {
+       remainingVert--;
+
+       attribs[8*vertex  ] = attribs[8*vertex-8];
+       attribs[8*vertex+1] = attribs[8*vertex-7];
+       attribs[8*vertex+2] = attribs[8*vertex-6];
+       attribs[8*vertex+3] = attribs[8*vertex-5];
+       attribs[8*vertex+4] = attribs[8*vertex-4];
+       attribs[8*vertex+5] = attribs[8*vertex-3];
+       attribs[8*vertex+6] = attribs[8*vertex-2];
+       attribs[8*vertex+7] = attribs[8*vertex-1];
+         
+       vertex++;     
+       }
+     
+     return vertex;
+     }
+   
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int buildSideGrid(int vertex, float[] attribs)
+     {
+     //android.util.Log.d("CUBES", "buildSide");
+
+     for(int i=0; i<mEdgeNum; i++)
+       {
+       vertex = buildIthSide(mEdges.get(i), vertex, attribs);
+       } 
+      
+     return vertex;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int buildIthSide(Edge curr, int vertex, float[] attribs)
+     {
+     Edge prev, next;
+     int col, row, side;
+
+     if( curr.side==NORTH ) // water outside
+       {
+       prev = new Edge(WEST,curr.row,curr.col);
+       }
+     else                   // land outside; we need to move forward one link because we are
+       {                    // going in opposite direction and we need to start from a bend.
+       prev = curr;
+       curr = new Edge(EAST,curr.row+1,curr.col-1);
+       }
+
+     for(int i=0; i<mSlices; i++)
+       {
+       col = curr.col;
+       row = curr.row;
+       side= curr.side;
+       next = getNextEdge(curr);
+     
+       addSideVertex(curr,true,i+1,prev.side,vertex++,attribs);
+
+       do
+         {
+         if( prev.side!=curr.side )
+           {
+           addSideVertex(curr,true,i+1,prev.side,vertex++,attribs);
+           addSideVertex(curr,true,i  ,prev.side,vertex++,attribs);
+           }
+       
+         addSideVertex(curr,false,i+1,next.side,vertex++,attribs);
+         addSideVertex(curr,false,i  ,next.side,vertex++,attribs);
+       
+         prev = curr;
+         curr = next;
+         next = getNextEdge(curr);
+         }
+       while( curr.col!=col || curr.row!=row || curr.side!=side );
+     
+       vertex = repeatLast(vertex,attribs);
+       }
+
+     return vertex;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private Edge getNextEdge(Edge curr)
+     {
+     int col = curr.col;
+     int row = curr.row;
+      
+     //android.util.Log.e("CUBES", "row="+row+" col="+col+" mRows="+mRows+" mCols="+mCols);
+                       
+     switch(curr.side) 
+       {
+       case NORTH: if( col==mCols-1 ) 
+                     return new Edge(EAST,row,col);
+                   if( row>0 && mCubes[row-1][col+1]==mCubes[row][col] )
+                     return new Edge(WEST,row-1,col+1);
+                   if( mCubes[row][col+1]==mCubes[row][col] )
+                     return new Edge(NORTH,row,col+1);
+                   else  
+                     return new Edge(EAST,row,col);
+                   
+       case SOUTH: if( col==0 ) 
+                     return new Edge(WEST,row,col);
+                   if( (row<mRows-1) && mCubes[row+1][col-1]==mCubes[row][col] )
+                     return new Edge(EAST,row+1,col-1); 
+                   if( mCubes[row][col-1]==mCubes[row][col] )
+                     return new Edge(SOUTH,row,col-1);
+                   else
+                     return new Edge(WEST,row,col); 
+                     
+       case EAST : if( row==mRows-1 ) 
+                     return new Edge(SOUTH,row,col);
+                   if( (col<mCols-1) && mCubes[row+1][col+1]==mCubes[row][col] )
+                     return new Edge(NORTH,row+1,col+1);
+                   if( mCubes[row+1][col]==mCubes[row][col] )
+                     return new Edge(EAST,row+1,col);
+                   else 
+                     return new Edge(SOUTH,row,col);
+                   
+       default   : if( row==0 )
+                     return new Edge(NORTH,row,col);
+                   if( col>0 && mCubes[row-1][col-1]==mCubes[row][col] )
+                     return new Edge(SOUTH,row-1,col-1);
+                   if( mCubes[row-1][col]==mCubes[row][col] )
+                     return new Edge(WEST,row-1,col);
+                   else
+                     return new Edge(NORTH,row,col);     
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int addFrontVertex(int vertex, int index, float vectZ, int col, int row, float[] attribs)
+     {
+     remainingVert--;
+
+     float x = (float)col/mCols;
+     float y = (float)row/mRows;
+
+     attribs[8*vertex  ] = x-0.5f;
+     attribs[8*vertex+1] = 0.5f-y;
+     attribs[8*vertex+2] = vectZ;
+     attribs[8*vertex+3] = mNormalX[index];
+     attribs[8*vertex+4] = mNormalY[index];
+     attribs[8*vertex+5] = mNormalZ[index];
+     attribs[8*vertex+6] = x;
+     attribs[8*vertex+7] = 1.0f-y;
+
+     return vertex+1;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+   
+  private void addSideVertex(Edge curr, boolean back, int slice,int side, int vertex, float[] attribs)
+     {
+     //android.util.Log.e("CUBES", "adding Side vertex!");
+
+     remainingVert--;
+
+     float x, y;
+
+     switch(curr.side)
+       {
+       case NORTH: x = (float)(back ? (curr.col  ):(curr.col+1))/mCols;
+
+                   attribs[8*vertex  ] = x - 0.5f;
+                   attribs[8*vertex+1] = 0.5f - (float)curr.row/mRows;
+                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
+                   attribs[8*vertex+3] = side==NORTH ? 0.0f : (side==WEST?-R:R);
+                   attribs[8*vertex+4] = 1.0f;
+                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
+                   attribs[8*vertex+6] = x;
+                   attribs[8*vertex+7] = 1.0f-(float)(curr.row-slice)/mRows;
+                   break;
+       case SOUTH: x = (float)(back ? (curr.col+1):(curr.col  ))/mCols;
+
+                   attribs[8*vertex  ] = x - 0.5f;
+                   attribs[8*vertex+1] = 0.5f - (float)(curr.row+1)/mRows;
+                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
+                   attribs[8*vertex+3] = side==SOUTH ? 0.0f: (side==EAST?-R:R);
+                   attribs[8*vertex+4] =-1.0f;
+                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
+                   attribs[8*vertex+6] = x;
+                   attribs[8*vertex+7] = 1.0f - (float)(curr.row+1+slice)/mRows;
+                   break;
+       case WEST : y = (float)(back  ? (curr.row+1):(curr.row))/mRows;
+
+                   attribs[8*vertex  ] = (float)curr.col/mCols -0.5f;
+                   attribs[8*vertex+1] = 0.5f - y;
+                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
+                   attribs[8*vertex+3] =-1.0f;
+                   attribs[8*vertex+4] = side==WEST ? 0.0f : (side==NORTH?-R:R);
+                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
+                   attribs[8*vertex+6] = (float)(curr.col-slice)/mCols;
+                   attribs[8*vertex+7] = 1.0f - y;
+                   break;
+       case EAST : y = (float)(back  ? (curr.row):(curr.row+1))/mRows;
+
+                   attribs[8*vertex  ] = (float)(curr.col+1)/mCols -0.5f;
+                   attribs[8*vertex+1] = 0.5f - y;
+                   attribs[8*vertex+2] = 0.5f - (float)slice/mSlices;
+                   attribs[8*vertex+3] = 1.0f;
+                   attribs[8*vertex+4] = side==EAST ? 0.0f : (side==SOUTH?-R:R);
+                   attribs[8*vertex+5] = (slice==0 ? R : (slice==mSlices ? -R:0) );
+                   attribs[8*vertex+6] = (float)(curr.col+1+slice)/mCols;
+                   attribs[8*vertex+7] = 1.0f - y;
+                   break;
+       }
+     
+     if(attribs[8*vertex+6]>1.0f) attribs[8*vertex+6] = 2.0f-attribs[8*vertex+6];
+     if(attribs[8*vertex+6]<0.0f) attribs[8*vertex+6] =     -attribs[8*vertex+6];
+     if(attribs[8*vertex+7]>1.0f) attribs[8*vertex+7] = 2.0f-attribs[8*vertex+7];
+     if(attribs[8*vertex+7]<0.0f) attribs[8*vertex+7] =     -attribs[8*vertex+7];
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void build()
+     {
+     int vertSoFar=0;
+     float[] attribs= new float[(POS_DATA_SIZE+NOR_DATA_SIZE+TEX_DATA_SIZE)*numVertices];
+
+     //android.util.Log.d("MeshCubes","building front grid...");
+
+     vertSoFar = buildFrontBackGrid(true, vertSoFar,attribs);
+
+     if( mSlices>0 )
+       {
+       vertSoFar = repeatLast(vertSoFar,attribs);
+       if( vertSoFar%2==1 )
+         {
+         //android.util.Log.d("MeshCubes","repeating winding1 vertex");
+
+         vertSoFar = repeatLast(vertSoFar,attribs);
+         }
+
+       //android.util.Log.d("MeshCubes","building side grid...");
+
+       vertSoFar = buildSideGrid (vertSoFar,attribs);
+
+       //android.util.Log.d("MeshCubes","building back grid...");
+
+       buildFrontBackGrid (false,vertSoFar,attribs);
+       }
+
+     //android.util.Log.e("MeshCubes", "dataLen="+numVertices);
+     //android.util.Log.d("MeshCubes", "attribs: "+debug(attribs,8) );
+
+     mEdges.clear();
+     mEdges = null;
+     mCubes = null;
+
+     if( remainingVert!=0 )
+       android.util.Log.e("MeshCubes", "remainingVert " +remainingVert );
+
+     mVertAttribs = ByteBuffer.allocateDirect(numVertices*VERTSIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
+     mVertAttribs.put(attribs).position(0);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Creates the underlying mesh of vertices, normals, texture coords.
+ *    
+ * @param cols   Integer helping to parse the next parameter.
+ * @param desc   String describing the subset of a MxNx1 cuboid that we want to create.
+ *               Its MxN characters - all 0 or 1 - decide of appropriate field is taken or not.
+ *               <p></p>
+ *               <p>
+ *               <pre>
+ *               For example, (cols=2, desc="111010") describes the following shape:
+ *
+ *               XX
+ *               X
+ *               X
+ *
+ *               whereas (cols=2,desc="110001") describes
+ *
+ *               XX
+ *
+ *                X
+ *               </pre>
+ *               </p>
+ * @param slices Number of slices, i.e. 'depth' of the Mesh.
+ */
+ public MeshCubes(int cols, String desc, int slices)
+   {
+   super( (float)slices/cols);
+   prepareDataStructures(cols,desc,slices);
+   build();
+   }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Creates a full, hole-less underlying mesh of vertices, normals, texture coords and colors.
+ *
+ * @param cols   Number of columns, i.e. 'width' of the Mesh.
+ * @param rows   Number of rows, i.e. 'height' of the Mesh.
+ * @param slices Number of slices, i.e. 'depth' of the Mesh.
+ */
+ public MeshCubes(int cols, int rows, int slices)
+   {
+   super( (float)slices/cols);
+   prepareDataStructures(cols,rows,slices);
+   build();
+   }
+ }
diff --git a/src/main/java/org/distorted/library/main/MeshFlat.java b/src/main/java/org/distorted/library/main/MeshFlat.java
new file mode 100644
index 0000000..1a89a1c
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/MeshFlat.java
@@ -0,0 +1,193 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Create a flat, rectangular grid.
+ * <p>
+ * Perfect if you just want to display a flat Texture. If you are not planning to apply any VERTEX
+ * effects to it, use MeshFlat(1,1), i.e. a Quad. Otherwise, create more vertices for more realistic effects!
+ */
+public class MeshFlat extends MeshObject
+  {
+  private int mCols, mRows;
+  private int remainingVert;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Create a flat, full grid.
+
+  private void computeNumberOfVertices(int cols, int rows)
+     {
+     mRows=rows;
+     mCols=cols;
+
+     if( cols==1 && rows==1 )
+       {
+       numVertices = 4;
+       }
+     else
+       {
+       numVertices = 2*( mRows*mCols +2*mRows - 1) +2*(mCols>=2 ? mRows:0) +
+                     (mCols>=2 && mRows>=2 ? 2*mRows-2 : 1);
+       }
+
+     //android.util.Log.e("BITMAP","vertices="+numVertices+" rows="+mRows+" cols="+mCols);
+
+     remainingVert = numVertices;
+     }
+
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int addVertex(int vertex, float x, float y, float[] attribs)
+     {
+     remainingVert--;
+
+     attribs[8*vertex  ] = x-0.5f;
+     attribs[8*vertex+1] = 0.5f-y;
+     attribs[8*vertex+2] = 0;
+
+     attribs[8*vertex+3] = 0.0f;
+     attribs[8*vertex+4] = 0.0f;
+     attribs[8*vertex+5] = 1.0f;
+
+     attribs[8*vertex+6] = x;
+     attribs[8*vertex+7] = 1.0f-y;
+
+     return vertex+1;
+     }
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int repeatLast(int vertex, float[] attribs)
+     {
+     remainingVert--;
+
+     //android.util.Log.e("BITMAP", "repeating last vertex!");
+
+     if( vertex>0 )
+       {
+       attribs[8*vertex  ] = attribs[8*vertex-8];
+       attribs[8*vertex+1] = attribs[8*vertex-7];
+       attribs[8*vertex+2] = attribs[8*vertex-6];
+       attribs[8*vertex+3] = attribs[8*vertex-5];
+       attribs[8*vertex+4] = attribs[8*vertex-4];
+       attribs[8*vertex+5] = attribs[8*vertex-3];
+       attribs[8*vertex+6] = attribs[8*vertex-2];
+       attribs[8*vertex+7] = attribs[8*vertex-1];
+
+       vertex++;
+       }
+
+     return vertex;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void buildGrid(float[] attribs)
+     {
+     boolean lastBlockIsNE = false;
+     boolean currentBlockIsNE;
+     int vertex = 0;
+
+     float x,y;
+     final float X = 1.0f/mCols;
+     final float Y = 1.0f/mRows;
+
+     //android.util.Log.d("BITMAP", "buildGrid");
+
+     y = 0.0f;
+
+     for(int row=0; row<mRows; row++)
+       {
+       x = 0.0f;
+
+       for(int col=0; col<mCols; col++)
+         {
+         currentBlockIsNE = (2*row<=mRows-1)^(2*col<=mCols-1);
+
+         if( col==0 || (lastBlockIsNE^currentBlockIsNE) )
+           {
+           if( row!=0 && col==0 ) vertex = repeatLast(vertex,attribs);
+           vertex= addVertex( vertex, x, y+(currentBlockIsNE?0:Y), attribs);
+           if( row!=0 && col==0 ) vertex = repeatLast(vertex,attribs);
+           if( lastBlockIsNE^currentBlockIsNE)  vertex = repeatLast(vertex,attribs);
+           vertex= addVertex( vertex, x, y+(currentBlockIsNE?Y:0), attribs);
+           }
+         vertex= addVertex( vertex, x+X, y+(currentBlockIsNE?0:Y), attribs);
+         vertex= addVertex( vertex, x+X, y+(currentBlockIsNE?Y:0), attribs);
+
+         lastBlockIsNE = currentBlockIsNE;
+         x+=X;
+         }
+
+       y+=Y;
+       }
+
+     //android.util.Log.d("BITMAP", "buildGrid done");
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/*
+  private static String debug(float[] val, int stop)
+     {
+     String ret="";
+
+     for(int i=0; i<val.length; i++)
+        {
+        if( i%stop==0 ) ret+="\n";
+        ret+=(" "+val[i]);
+        }
+
+     return ret;
+     }
+*/
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Creates the underlying grid of vertices, normals and texture coords.
+ *
+ * @param cols Number of columns in the grid.
+ * @param rows Number of rows in the grid.
+ */
+ public MeshFlat(int cols, int rows)
+    {
+    super(0.0f);
+    computeNumberOfVertices(cols,rows);
+
+    float[] attribs= new float[(POS_DATA_SIZE+NOR_DATA_SIZE+TEX_DATA_SIZE)*numVertices];
+
+    buildGrid(attribs);
+
+    //android.util.Log.e("MeshFlat", "dataLen="+numVertices);
+    //android.util.Log.d("MeshFlat", "attribs: "+debug(attribs,8) );
+
+    if( remainingVert!=0 )
+      android.util.Log.d("BITMAP", "remainingVert " +remainingVert );
+
+    mVertAttribs = ByteBuffer.allocateDirect(numVertices*VERTSIZE).order(ByteOrder.nativeOrder()).asFloatBuffer();
+    mVertAttribs.put(attribs).position(0);
+    }
+ }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/library/main/MeshObject.java b/src/main/java/org/distorted/library/main/MeshObject.java
new file mode 100644
index 0000000..dfe7106
--- /dev/null
+++ b/src/main/java/org/distorted/library/main/MeshObject.java
@@ -0,0 +1,142 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2016 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Distorted.                                                               //
+//                                                                                               //
+// Distorted 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 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Distorted 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 Distorted.  If not, see <http://www.gnu.org/licenses/>.                            //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.library.main;
+
+import android.opengl.GLES30;
+
+import java.nio.FloatBuffer;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * Abstract class which represents a Mesh, ie 3 arrays of Vertex attributes: 1) positions
+ * 2) normals 3) texture coordinates.
+ * <p>
+ * If you want to render to a particular shape, extend from here, construct the attrib FloatBuffer
+ * and provide correct numVertices.
+ */
+public abstract class MeshObject extends DistortedObject
+   {
+   private static final int BYTES_PER_FLOAT = 4;
+
+   static final int POS_DATA_SIZE= 3;
+   static final int NOR_DATA_SIZE= 3;
+   static final int TEX_DATA_SIZE= 2;
+
+   static final int OFFSET0 =                                                           0;
+   static final int OFFSET1 = (POS_DATA_SIZE                            )*BYTES_PER_FLOAT;
+   static final int OFFSET2 = (POS_DATA_SIZE+NOR_DATA_SIZE              )*BYTES_PER_FLOAT;
+
+   static final int TFSIZE  = (POS_DATA_SIZE+POS_DATA_SIZE              )*BYTES_PER_FLOAT;
+   static final int VERTSIZE= (POS_DATA_SIZE+NOR_DATA_SIZE+TEX_DATA_SIZE)*BYTES_PER_FLOAT;
+
+   boolean mShowNormals;
+
+   int numVertices;
+   FloatBuffer mVertAttribs;   // packed: PosX,PosY,PosZ, NorX, NorY,NorZ, TexS, TexT
+   int[] mAttVBO = new int[1]; // server-side packed vertex attributes
+   int[] mAttTFO = new int[1]; // transform feedback
+
+   final float zFactor;        // strange workaround for the fact that we need to somehow store the 'depth'
+                               // of the Mesh. Used in DistortedEffects. See DistortedTexture.getDepth().
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   MeshObject(float factor)
+     {
+     super(DistortedObject.NOT_CREATED_YET,DistortedObject.TYPE_USER);
+
+     zFactor = factor;
+     mShowNormals = false;
+     recreate();
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// must be called from a thread holding OpenGL Context
+//
+// Do NOT release mVertAttribs etc as we will need them when we need to re-create the buffers after
+// a loss of OpenGL context!
+
+   void create()
+     {
+     if( mAttVBO[0]<0 )
+       {
+       GLES30.glGenBuffers(1, mAttVBO, 0);
+       GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, mAttVBO[0]);
+       GLES30.glBufferData(GLES30.GL_ARRAY_BUFFER, numVertices*VERTSIZE, mVertAttribs, GLES30.GL_STATIC_READ);
+       GLES30.glBindBuffer(GLES30.GL_ARRAY_BUFFER, 0);
+       }
+     if( mAttTFO[0]<0 && Distorted.GLSL >= 300 )
+       {
+       GLES30.glGenBuffers(1, mAttTFO, 0);
+       GLES30.glBindBuffer(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, mAttTFO[0]);
+       GLES30.glBufferData(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, numVertices*TFSIZE, null, GLES30.GL_STATIC_READ);
+       GLES30.glBindBuffer(GLES30.GL_TRANSFORM_FEEDBACK_BUFFER, 0);
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// must be called from a thread holding OpenGL Context
+
+   void delete()
+     {
+     if( mAttVBO[0]>=0 )
+       {
+       GLES30.glDeleteBuffers(1, mAttVBO, 0);
+       mAttVBO[0] = -1;
+       }
+     if( mAttTFO[0]>=0 )
+       {
+       GLES30.glDeleteBuffers(1, mAttTFO, 0);
+       mAttTFO[0] = -1;
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   void recreate()
+     {
+     mAttVBO[0] = -1;
+     mAttTFO[0] = -1;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// debugging only
+
+   String printDetails()
+     {
+     return getClass().getSimpleName()+" vertices:"+ numVertices;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+/**
+ * When rendering this Mesh, do we want to render the Normal vectors as well?
+ * <p>
+ * Will work only on OpenGL ES >= 3.0 devices.
+ *
+ * @param show Controls if we render the Normal vectors or not.
+ */
+   public void setShowNormals(boolean show)
+     {
+     mShowNormals = (Distorted.GLSL >= 300 && show);
+     }
+   }
+
+
+
diff --git a/src/main/java/org/distorted/library/message/EffectListener.java b/src/main/java/org/distorted/library/message/EffectListener.java
index 2523755..dc0d4a2 100644
--- a/src/main/java/org/distorted/library/message/EffectListener.java
+++ b/src/main/java/org/distorted/library/message/EffectListener.java
@@ -21,9 +21,11 @@ package org.distorted.library.message;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+import org.distorted.library.main.DistortedEffects;
+
 /**
  * This interface lets users of the Distorted library get notified when something happens to one of the effects.
- * To receive the notifications, we first have to register with a call to {@link org.distorted.library.DistortedEffects#registerForMessages(EffectListener)}.
+ * To receive the notifications, we first have to register with a call to {@link DistortedEffects#registerForMessages(EffectListener)}.
  * List of all possible events that can happen is defined in {@link EffectMessage}
  */
 
@@ -36,7 +38,7 @@ public interface EffectListener
  * @param effectID   ID of the effect the event happened to. This ID must have been previously returned by one
  *                   of the DistortedEffects.{deform,distort,move,...} functions.
  * @param effectName Name of the effect as defined by final constants from the 4 classes descendant from Effect.
- * @param objectID   the ID of the DistortedEffects object, as returned by {@link org.distorted.library.DistortedEffects#getID()},
+ * @param objectID   the ID of the DistortedEffects object, as returned by {@link DistortedEffects#getID()},
  *                   this event happened to.
  * @see EffectMessage
  */
diff --git a/src/main/java/org/distorted/library/message/EffectMessage.java b/src/main/java/org/distorted/library/message/EffectMessage.java
index a1ec8b1..0f3416a 100644
--- a/src/main/java/org/distorted/library/message/EffectMessage.java
+++ b/src/main/java/org/distorted/library/message/EffectMessage.java
@@ -21,6 +21,8 @@ package org.distorted.library.message;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
+import org.distorted.library.main.DistortedEffects;
+
 /**
 * Defines all possible events a class implementing the {@link EffectListener} interface can receive.
 */
@@ -30,7 +32,7 @@ public enum EffectMessage
 /**
  * The effect has been removed. This can happen if:
  * <ul>
- * <li> someone explicitly removed the effect with a call to {@link org.distorted.library.DistortedEffects#abortEffect(long)}
+ * <li> someone explicitly removed the effect with a call to {@link DistortedEffects#abortEffect(long)}
  *      (or one of the other 'abort' methods)
  * <li> the interpolation of the effect has finished and the end result is equal to the effect's unity.
  * </ul>    
diff --git a/src/main/java/org/distorted/library/program/DistortedProgram.java b/src/main/java/org/distorted/library/program/DistortedProgram.java
index dccc30f..a89f02c 100644
--- a/src/main/java/org/distorted/library/program/DistortedProgram.java
+++ b/src/main/java/org/distorted/library/program/DistortedProgram.java
@@ -22,7 +22,7 @@ package org.distorted.library.program;
 import android.opengl.GLES30;
 import android.os.Build;
 
-import org.distorted.library.DistortedEffects;
+import org.distorted.library.main.DistortedEffects;
 
 import java.io.BufferedReader;
 import java.io.IOException;
diff --git a/src/main/java/org/distorted/library/program/FragmentCompilationException.java b/src/main/java/org/distorted/library/program/FragmentCompilationException.java
index ff78a08..adbba4d 100644
--- a/src/main/java/org/distorted/library/program/FragmentCompilationException.java
+++ b/src/main/java/org/distorted/library/program/FragmentCompilationException.java
@@ -20,8 +20,11 @@
 package org.distorted.library.program;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.library.main.Distorted;
+
 /**
- *  Thrown by {@link org.distorted.library.Distorted#onCreate(android.content.Context)}
+ *  Thrown by {@link Distorted#onCreate(android.content.Context)}
  *  if compilation of the fragment shader fails for some other reason than too many uniforms.
  *  <p>
  *  This can happen on older OpenGL ES 2.0 devices if they, say, do not support variable loops in the shaders.
diff --git a/src/main/java/org/distorted/library/program/FragmentUniformsException.java b/src/main/java/org/distorted/library/program/FragmentUniformsException.java
index 95f64d1..ee883f0 100644
--- a/src/main/java/org/distorted/library/program/FragmentUniformsException.java
+++ b/src/main/java/org/distorted/library/program/FragmentUniformsException.java
@@ -20,10 +20,13 @@
 package org.distorted.library.program;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.library.main.Distorted;
+
 /**
- *  Thrown by {@link org.distorted.library.Distorted#onCreate(android.content.Context)}
+ *  Thrown by {@link Distorted#onCreate(android.content.Context)}
  *  if compilation of the fragment shader fails because of too many uniforms there, i.e. because
- *  we have set {@link org.distorted.library.Distorted#setMaxFragment(int)} to too high value.
+ *  we have set {@link Distorted#setMaxFragment(int)} to too high value.
  */
 
 @SuppressWarnings("serial")
diff --git a/src/main/java/org/distorted/library/program/LinkingException.java b/src/main/java/org/distorted/library/program/LinkingException.java
index f14e497..8a0c2a6 100644
--- a/src/main/java/org/distorted/library/program/LinkingException.java
+++ b/src/main/java/org/distorted/library/program/LinkingException.java
@@ -20,8 +20,11 @@
 package org.distorted.library.program;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.library.main.Distorted;
+
 /**
- *  Thrown by {@link org.distorted.library.Distorted#onCreate(android.content.Context)}
+ *  Thrown by {@link Distorted#onCreate(android.content.Context)}
  *  if linking of the Shaders fails.
  *  <p>
  *  Theoretically this should never happen.
diff --git a/src/main/java/org/distorted/library/program/VertexCompilationException.java b/src/main/java/org/distorted/library/program/VertexCompilationException.java
index ac8b1c5..ed0d286 100644
--- a/src/main/java/org/distorted/library/program/VertexCompilationException.java
+++ b/src/main/java/org/distorted/library/program/VertexCompilationException.java
@@ -20,8 +20,11 @@
 package org.distorted.library.program;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.library.main.Distorted;
+
 /**
- *  Thrown by {@link org.distorted.library.Distorted#onCreate(android.content.Context)}
+ *  Thrown by {@link Distorted#onCreate(android.content.Context)}
  *  if compilation of the vertex shader fails for some other reason than too many uniforms.
  *  <p>
  *  This can happen on older OpenGL ES 2.0 devices if they, say, do not support variable loops in the shaders.
diff --git a/src/main/java/org/distorted/library/program/VertexUniformsException.java b/src/main/java/org/distorted/library/program/VertexUniformsException.java
index da02370..b81aa61 100644
--- a/src/main/java/org/distorted/library/program/VertexUniformsException.java
+++ b/src/main/java/org/distorted/library/program/VertexUniformsException.java
@@ -20,10 +20,13 @@
 package org.distorted.library.program;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
+
+import org.distorted.library.main.Distorted;
+
 /**
- *  Thrown by {@link org.distorted.library.Distorted#onCreate(android.content.Context)}
+ *  Thrown by {@link Distorted#onCreate(android.content.Context)}
  *  if compilation of the Vertex Shader fails because of too many uniforms there, i.e. because
- *  we have set {@link org.distorted.library.Distorted#setMaxVertex(int)} to too high value.
+ *  we have set {@link Distorted#setMaxVertex(int)} to too high value.
  */
 
 @SuppressWarnings("serial")
