public abstract class GLProgram {
	private int fps;
	
	/**
	 * Initializes the application in fullscreen mode.
	 * 
	 * @param vsync Enables/disables the vertical-sync feature, where the rendering is in sync with the monitor's refresh rate.
	 *        With v-sync off, there is no framerate cap and the gameloop will run as fast as the hardware can handle.
	 *        A framerate can be set with the <code>setFPS(int fps)</code> method.
	 */
	public GLProgram(boolean vsync) {
		try {
			Display.setFullscreen(true);
			Display.setVSyncEnabled(vsync);
		} catch(Exception exc) {
			exc.printStackTrace();
		}
	}
	
	/**
	 * Initializes a windowed application. The framerate is set to 60 and can be modified using <code>setFPS(int fps)</code>.
	 * 
	 * @param name The title of the window.
	 * @param width The width of the window.
	 * @param height The height of the window.
	 * @param resizable Enables/disables the ability to resize the window.
	 */
	public GLProgram(String name, int width, int height, boolean resizable) {
		Display.setTitle(name);
		
		try {
			Display.setDisplayMode(new DisplayMode(width, height));
		} catch(Exception exc) {
			exc.printStackTrace();
		}
		
		Display.setResizable(resizable);
		
		fps = 60;
	}
	
	/**
	 * Sets the framerate of the game loop.
	 * 
	 * @param fps The desired framerate, in frames per second. It must be a positive integer or 0.
	 *        Specifying 0 means no rendering cap. Specifying a negative value will result in undefined behavior.
	 */
	public void setFPS(int fps) {
		this.fps = fps;
	}
	
	/**
	 * Returns the framerate of the game loop.
	 * 
	 * @return The framerate, in frames per second.
	 */
	public int getFPS() {
		return fps;
	}
	
	/**
	 * Initializes the context with default settings, which is generally the latest OpenGL version supported and compatibility
	 * profile if supported. For Mac OS X, the core profile must be specifically requested therefore this method is not
	 * sufficient.
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * Equivalent to <code>run(false)</code>.
	 */
	public final void run() {
		run(false);
	}
	
	/**
	 * Initializes the context depending on the value of <code>core</code>. Supplying <code>false</code> is equivalent to
	 * simply calling <code>run()</code>, while <code>true</code> requests the Core profile if available.
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param core <code>True</code> requests the Core profile, while <code>false</code> keeps default settings.
	 */
	public final void run(boolean core) {
		run(core, new PixelFormat());
	}
	
	/**
	 * Initializes the context depending on the value of <code>core</code> and the supplied PixelFormat.
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param core <code>True</code> requests the Core profile, while <code>false</code> keeps default settings.
	 * @param format The PixelFormat specifying the buffers.
	 */
	public final void run(boolean core, PixelFormat format) {
		run(format, core ? new ContextAttribs(3, 2).withProfileCore(true) : null);
	}
	
	/**
	 * Initializes the context requesting the specified OpenGL version in the format: <code>major.minor</code>
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param major
	 * @param minor
	 */
	public final void run(int major, int minor) {
		run(major, minor, false);
	}
	
	/**
	 * Initializes the context depending on the value of <code>core</code> and the specified OpenGL version in the format: <code>major.minor</code>
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param major
	 * @param minor
	 * @param core <code>True</code> requests the Core profile, while <code>false</code> requests the Compatibility profile.
	 */
	public final void run(int major, int minor, boolean core) {
		run(major, minor, core, new PixelFormat());
	}
	
	/**
	 * Initializes the context depending on the value of <code>core</code>, the supplied PixelFormat, and the specified
	 * OpenGL version in the format: <code>major.minor</code>
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param major
	 * @param minor
	 * @param core <<code>True</code> requests the Core profile, while <code>false</code> requests the Compatibility profile.
	 * @param format The PixelFormat specifying the buffers.
	 */
	public final void run(int major, int minor, boolean core, PixelFormat format) {
		run(format, core ? new ContextAttribs(major, minor).withProfileCore(core) : new ContextAttribs(major, minor));
	}
	
	/**
	 * Initializes the context with default settings and the supplied PixelFormat.
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param format The PixelFormat specifying the buffers.
	 */
	public final void run(PixelFormat format) {
		run(format, null);
	}
	
	/**
	 * Initializes the context with its attributes supplied.
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param attribs The context attributes.
	 */
	public final void run(ContextAttribs attribs) {
		run(new PixelFormat(), attribs);
	}
	
	/**
	 * Initializes the context with its attributes and PixelFormat supplied.
	 * 
	 * This method does not return until the game loop ends.
	 * 
	 * @param format The PixelFormat specifying the buffers.
	 * @param attribs The context attributes.
	 */
	public final void run(PixelFormat format, ContextAttribs attribs) {
		try {
			Display.create(format, attribs);
		} catch(Exception exc) {
			exc.printStackTrace();
			System.exit(1);
		}
		
		gameLoop();
	}
	
	private void gameLoop() {
		try {
			init();
			
			Utils.checkGLError("init");
			
			resized();
			
			Utils.checkGLError("resized");
			
			long lastTime, lastFPS;
			lastTime = lastFPS = System.nanoTime();
			int frames = 0;
			
			while(!shouldStop()) {
				long deltaTime = System.nanoTime() - lastTime;
				lastTime += deltaTime;
				
				if(Display.wasResized())
					resized();
				
				while(Keyboard.next()) {
					if(Keyboard.getEventKeyState())
						keyPressed(Keyboard.getEventKey(), Keyboard.getEventCharacter());
					else
						keyReleased(Keyboard.getEventKey(), Keyboard.getEventCharacter());
				}
				
				update(deltaTime);
				
				Utils.checkGLError("update");
				
				render();
				
				Utils.checkGLError("render");
				
				Display.update();
				
				frames++;
				if(System.nanoTime() - lastFPS >= 1e9) {
					System.out.println("FPS: ".concat(String.valueOf(frames)));
					lastFPS += 1e9;
					frames = 0;
				}
				
				Display.sync(fps);
			}
		} catch(Throwable exc) {
			exc.printStackTrace();
		} finally {
			destroy();
		}
	}
	
	/**
	 * Returns the width of the window.
	 * 
	 * @return The width of the window.
	 */
	public int getWidth() {
		return Display.getWidth();
	}
	
	/**
	 * Returns the height of the window.
	 * 
	 * @return The height of the window.
	 */
	public int getHeight() {
		return Display.getHeight();
	}
	
	/**
	 * Called at most once after one of the <code>run</code> methods are called. This method
	 * must be implemented by user code.
	 */
	public abstract void init();
	
	/**
	 * Called when the window is resized. This method updates the <code>glViewport</code> but may be overridden
	 * with custom code. Make sure to call <code>super.resized()</code> if overriding, or remember to manually update
	 * the <code>glViewport</code>!
	 */
	public void resized() {
		glViewport(0, 0, getWidth(), getHeight());
	}
	
	/**
	 * Consistently polled once per frame to test whether the game loop should stop. This
	 * method checks if either the window's close button has been clicked, ALT-F4 has been pressed, or if the ESCAPE key is pressed.
	 * 
	 * @return Returns <code>true</code> if the game loop should stop, otherwise <code>false</code>.
	 */
	public boolean shouldStop() {
		return Display.isCloseRequested() || Keyboard.isKeyDown(Keyboard.KEY_ESCAPE);
	}
	
	/**
	 * Called when a key has been pressed.
	 * 
	 * @param key The @see org.lwjgl.input.Keyboard keycode of the pressed key.
	 * @param c The literal character of the pressed key.
	 */
	public void keyPressed(int key, char c) {}
	
	/**
	 * Called when a key has been released.
	 *
	 * @param key The @see org.lwjgl.input.Keyboard keycode of the released key.
	 * @param c The literal character of the released key.
	 */
	public void keyReleased(int key, char c) {}
	
	/**
	 * Called once per frame and given the elapsed time since the last call
	 * to this method.
	 * 
	 * @param deltaTime The elapsed time since the last call to this method, in nanoseconds.
	 */
	public void update(long deltaTime) {}
	
	/**
	 * Called once per frame and must be implemented by user code. Render operations should
	 * occur here.
	 */
	public abstract void render();
	
	/**
	 * Destroys the context.
	 */
	public void destroy() {
		Display.destroy();
	}
	
	/**
	 * Returns the entire contents of the file in a String. The file should be specified as relative to the user class
	 * that extends GLProgram.
	 * 
	 * @param file The file path, relative to the user class extending GLProgram.
	 * @return The entire contents of the file.
	 */
	protected String readFromFile(String file) {
		try {
			return Utils.readFully(getClass().getResourceAsStream(file));
		} catch(Exception exc) {
			throw new RuntimeException("Failure reading file " + file, exc);
		}
	}
}
 public class LightManager {
	public static final int NUMBER_OF_LIGHTS = 4;
	public static final int NUMBER_OF_POINT_LIGHTS = NUMBER_OF_LIGHTS - 1;
	
	private Timer sunTimer;
	private TimedLinearInterpolatorVector<Vector4> ambientInterpolator;
	private TimedLinearInterpolatorVector<Vector4> backgroundInterpolator;
	private TimedLinearInterpolatorVector<Vector4> sunlightInterpolator;
	private TimedLinearInterpolatorf maxIntensityInterpolator;
	
	private ArrayList<ConstVelLinearInterpolatorVector<Vector3>> lightPos;
	private ArrayList<Vector4> lightIntensity;
	private ArrayList<Timer> lightTimers;
	private HashMap<String,Timer> extraTimers;
	
	private final float halfLightDistance = 70;
	private float lightAttenuation = 1f / (halfLightDistance * halfLightDistance);
	
	private float lightHeight = 10.5f;
	private float lightRadius = 70;
	
	public LightManager() {
		sunTimer = new Timer(Type.LOOP, 30);
		ambientInterpolator = new TimedLinearInterpolatorVector<>();
		backgroundInterpolator = new TimedLinearInterpolatorVector<>();
		sunlightInterpolator = new TimedLinearInterpolatorVector<>();
		maxIntensityInterpolator = new TimedLinearInterpolatorf();
		
		extraTimers = new HashMap<>();
		
		lightTimers = new ArrayList<>();
		
		lightPos = new ArrayList<>();
		for(int a = 0; a < NUMBER_OF_POINT_LIGHTS; a++)
			lightPos.add(new ConstVelLinearInterpolatorVector<Vector3>());
		
		lightIntensity = new ArrayList<>();
		for(int a = 0; a < NUMBER_OF_POINT_LIGHTS; a++)
			lightIntensity.add(new Vector4(0.2f, 0.2f, 0.2f, 1));
		
		ArrayList<Vector3> posValues = new ArrayList<>();
		posValues.add(new Vector3(-50, 30, 70));
		posValues.add(new Vector3(-70, 30, 50));
		posValues.add(new Vector3(-70, 30, -50));
		posValues.add(new Vector3(-50, 30, -70));
		posValues.add(new Vector3(50, 30, -70));
		posValues.add(new Vector3(70, 30, -50));
		posValues.add(new Vector3(70, 30, 50));
		posValues.add(new Vector3(50, 30, 70));
		lightPos.get(0).setValues(posValues);
		lightTimers.add(new Timer(Timer.Type.LOOP, 15));
		
		posValues.clear();
		posValues.add(new Vector3(100, 6, 75));
		posValues.add(new Vector3(90, 8, 90));
		posValues.add(new Vector3(75, 10, 100));
		posValues.add(new Vector3(60, 12, 90));
		posValues.add(new Vector3(50, 14, 75));
		posValues.add(new Vector3(60, 16, 60));
		posValues.add(new Vector3(75, 18, 50));
		posValues.add(new Vector3(90, 20, 60));
		posValues.add(new Vector3(100, 22, 75));
		posValues.add(new Vector3(90, 24, 90));
		posValues.add(new Vector3(75, 26, 100));
		posValues.add(new Vector3(60, 28, 90));
		posValues.add(new Vector3(50, 30, 75));
		
		posValues.add(new Vector3(105, 9, -70));
		posValues.add(new Vector3(105, 10, -90));
		posValues.add(new Vector3(72, 20, -90));
		posValues.add(new Vector3(72, 22, -70));
		posValues.add(new Vector3(105, 32, -70));
		posValues.add(new Vector3(105, 34, -90));
		posValues.add(new Vector3(72, 44, -90));
		
		lightPos.get(1).setValues(posValues);
		lightTimers.add(new Timer(Timer.Type.LOOP, 25));
		
		posValues.clear();
		posValues.add(new Vector3(-7, 35, 1));
		posValues.add(new Vector3(8, 40, -14));
		posValues.add(new Vector3(-7, 45, -29));
		posValues.add(new Vector3(-22, 50, -14));
		posValues.add(new Vector3(-7, 55, 1));
		posValues.add(new Vector3(8, 60, -14));
		posValues.add(new Vector3(-7, 65, -29));
		
		posValues.add(new Vector3(-83, 30, -92));
		posValues.add(new Vector3(-98, 27, -77));
		posValues.add(new Vector3(-83, 24, -62));
		posValues.add(new Vector3(-68, 21, -77));
		posValues.add(new Vector3(-83, 18, -92));
		posValues.add(new Vector3(-98, 15, -77));
		
		posValues.add(new Vector3(-50, 8, 25));
		posValues.add(new Vector3(-59.5f, 4, 65));
		posValues.add(new Vector3(-59.5f, 4, 78));
		posValues.add(new Vector3(-45, 4, 82));
		posValues.add(new Vector3(-40, 4, 50));
		posValues.add(new Vector3(-70, 20, 40));
		posValues.add(new Vector3(-60, 20, 90));
		posValues.add(new Vector3(-40, 25, 90));
		
		lightPos.get(2).setValues(posValues);
		lightTimers.add(new Timer(Timer.Type.LOOP, 15));
	}
	
	public Vector4 calcLightPosition(Timer timer, float alphaOffset) {
		float currTime = timer.getAlpha() + alphaOffset;
		
		Vector4 ret = new Vector4(0, lightHeight, 0, 1);
		ret.x((float)Math.cos(currTime * 2 * Math.PI) * lightRadius);
		ret.z((float)Math.sin(currTime * 2 * Math.PI) * lightRadius);
		
		return ret;
	}
	
	public void setSunlightValues(SunlightValue[] values) {
		ArrayList<TimedLinearInterpolatorVector<Vector4>.Data> ambient = new ArrayList<>();
		ArrayList<TimedLinearInterpolatorVector<Vector4>.Data> light = new ArrayList<>();
		ArrayList<TimedLinearInterpolatorVector<Vector4>.Data> background = new ArrayList<>();
		
		for(SunlightValue v : values) {
			ambient.add(ambientInterpolator.new Data(v.ambient, v.normTime));
			light.add(sunlightInterpolator.new Data(v.sunlightIntensity, v.normTime));
			background.add(backgroundInterpolator.new Data(v.backgroundColor, v.normTime));
		}
		
		ambientInterpolator.setValues(ambient);
		sunlightInterpolator.setValues(light);
		backgroundInterpolator.setValues(background);
		
		ArrayList<TimedLinearInterpolatorf.Data> maxIntensity = new ArrayList<>();
		maxIntensity.add(maxIntensityInterpolator.new Data(1, 0));
		maxIntensityInterpolator.setValues(maxIntensity, false);
	}
	
	public void setSunlightValues(SunlightValueHDR[] values) {
		ArrayList<TimedLinearInterpolatorVector<Vector4>.Data> ambient = new ArrayList<>();
		ArrayList<TimedLinearInterpolatorVector<Vector4>.Data> light = new ArrayList<>();
		ArrayList<TimedLinearInterpolatorVector<Vector4>.Data> background = new ArrayList<>();
		ArrayList<TimedLinearInterpolatorf.Data> maxIntensity = new ArrayList<>();
		
		for(SunlightValueHDR v : values) {
			ambient.add(ambientInterpolator.new Data(v.ambient, v.normTime));
			light.add(sunlightInterpolator.new Data(v.sunlightIntensity, v.normTime));
			background.add(backgroundInterpolator.new Data(v.backgroundColor, v.normTime));
			maxIntensity.add(maxIntensityInterpolator.new Data(v.maxIntensity, v.normTime));
		}
		
		ambientInterpolator.setValues(ambient);
		sunlightInterpolator.setValues(light);
		backgroundInterpolator.setValues(background);
		maxIntensityInterpolator.setValues(maxIntensity);
	}
	
	public void updateTime(long deltaTime) {
		sunTimer.update(deltaTime);
		for(Timer t : lightTimers)
			t.update(deltaTime);
		for(Timer t : extraTimers.values())
			t.update(deltaTime);
	}
	
	public void setPause(TimerTypes timer, boolean pause) {
		if(timer == TimerTypes.TIMER_ALL || timer == TimerTypes.TIMER_LIGHTS) {
			for(Timer t : lightTimers)
				t.setPause(pause);
			for(Timer t : extraTimers.values())
				t.setPause(pause);
		}
		
		if(timer == TimerTypes.TIMER_ALL || timer == TimerTypes.TIMER_SUN)
			sunTimer.togglePause();
	}
	
	public void togglePause(TimerTypes timer) {
		setPause(timer, !isPaused(timer));
	}
	
	public boolean isPaused(TimerTypes timer) {
		if(timer == TimerTypes.TIMER_ALL || timer == TimerTypes.TIMER_SUN)
			return sunTimer.isPaused();
		
		return lightTimers.get(0).isPaused();
	}
	
	public void rewindTime(TimerTypes timer, float secRewind) {
		if(timer == TimerTypes.TIMER_ALL || timer == TimerTypes.TIMER_SUN)
			sunTimer.rewind(secRewind);
		
		if(timer == TimerTypes.TIMER_ALL || timer == TimerTypes.TIMER_LIGHTS) {
			for(Timer t : lightTimers)
				t.rewind(secRewind);
			for(Timer t : extraTimers.values())
				t.rewind(secRewind);
		}
	}
	
	public void fastForwardTime(TimerTypes timer, float secFF) {
		if(timer == TimerTypes.TIMER_ALL || timer == TimerTypes.TIMER_SUN)
			sunTimer.fastForward(secFF);
		
		if(timer == TimerTypes.TIMER_ALL || timer == TimerTypes.TIMER_LIGHTS) {
			for(Timer t : lightTimers)
				t.fastForward(secFF);
			for(Timer t : extraTimers.values())
				t.fastForward(secFF);
		}
	}
	
	public LightBlock getLightInformation(Matrix4 worldToCameraMatrix) {
		LightBlock lightData = new LightBlock(ambientInterpolator.interpolate(sunTimer.getAlpha()), lightAttenuation);
		lightData.lights[0] = new PerLight(worldToCameraMatrix.mult(getSunlightDirection()), sunlightInterpolator.interpolate(sunTimer.getAlpha()));
		
		for(int light = 0; light < NUMBER_OF_POINT_LIGHTS; light++) {
			Vector4 worldLightPos = new Vector4(lightPos.get(light).interpolate(lightTimers.get(light).getAlpha()), 1);
			Vector4 lightPosCameraSpace = worldToCameraMatrix.mult(worldLightPos);
			
			lightData.lights[light + 1] = new PerLight(lightPosCameraSpace, lightIntensity.get(light));
		}
		
		return lightData;
	}
	
	public LightBlockHDR getLightInformationHDR(Matrix4 worldToCameraMatrix) {
		LightBlockHDR lightData = new LightBlockHDR(ambientInterpolator.interpolate(sunTimer.getAlpha()), lightAttenuation, maxIntensityInterpolator.interpolate(sunTimer.getAlpha()));
		lightData.lights[0] = new PerLight(worldToCameraMatrix.mult(getSunlightDirection()), sunlightInterpolator.interpolate(sunTimer.getAlpha()));
		
		for(int light = 0; light < NUMBER_OF_POINT_LIGHTS; light++) {
			Vector4 worldLightPos = new Vector4(lightPos.get(light).interpolate(lightTimers.get(light).getAlpha()), 1);
			Vector4 lightPosCameraSpace = worldToCameraMatrix.mult(worldLightPos);
			
			lightData.lights[light + 1] = new PerLight(lightPosCameraSpace, lightIntensity.get(light));
		}
		
		return lightData;
	}
	
	public LightBlockGamma getLightInformationGamma(Matrix4 worldToCameraMatrix) {
		LightBlockHDR lightDataHDR = getLightInformationHDR(worldToCameraMatrix);
		LightBlockGamma lightData = new LightBlockGamma(lightDataHDR.ambientIntensity, lightDataHDR.lightAttenuation, lightDataHDR.maxIntensity, 0);
		lightData.lights = lightDataHDR.lights;
		
		return lightData;
	}
	
	public Vector4 getSunlightDirection() {
		float angle = 2 * (float)Math.PI * sunTimer.getAlpha();
		Vector4 sunDirection = new Vector4(0);
		sunDirection.x((float)Math.sin(angle));
		sunDirection.y((float)Math.cos(angle));
		
		return new Matrix4().clearToIdentity().rotateDeg(5, 0, 1, 0).mult(sunDirection);
	}
	
	public Vector4 getSunlightIntensity() {
		return sunlightInterpolator.interpolate(sunTimer.getAlpha());
	}
	
	public int getNumberOfPointLights() {
		return lightPos.size();
	}
	
	public Vector3 getWorldLightPosition(int lightIndex) {
		return lightPos.get(lightIndex).interpolate(lightTimers.get(lightIndex).getAlpha());
	}
	
	public void setPointLightIntensity(int lightIndex, Vector4 intensity) {
		lightIntensity.set(lightIndex, intensity.copy());
	}
	
	public Vector4 getPointLightIntensity(int lightIndeX) {
		return lightIntensity.get(lightIndeX);
	}
	
	public void createTimer(String timerName, Timer.Type type, float duration) {
		extraTimers.put(timerName, new Timer(type, duration));
	}
	
	public float getTimerValue(String timerName) {
		Timer t = extraTimers.get(timerName);
		
		if(t == null)
			return -1;
		
		return t.getAlpha();
	}
	
	public Vector4 getBackgroundColor() {
		return backgroundInterpolator.interpolate(sunTimer.getAlpha());
	}
	
	public float getMaxIntensity() {
		return maxIntensityInterpolator.interpolate(sunTimer.getAlpha());
	}
	
	public float getSunTime() {
		return sunTimer.getAlpha();
	}
	
	public static class PerLight {
		public Vector4 cameraSpaceLightPos;
		public Vector4 lightIntensity;
		
		public static final int SIZE = 2 * 4 * 4;
		
		public PerLight(Vector4 cameraSpaceLightPos, Vector4 lightIntensity) {
			this.cameraSpaceLightPos = cameraSpaceLightPos;
			this.lightIntensity = lightIntensity;
		}
		
		private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(SIZE / 4);
		
		public FloatBuffer toBuffer() {
			buffer.clear();
			buffer.put(cameraSpaceLightPos.toBuffer());
			buffer.put(lightIntensity.toBuffer());
			buffer.flip();
			return buffer;
		}
	}
	
	public static class LightBlock {
		public Vector4 ambientIntensity;
		public float lightAttenuation;
		public PerLight[] lights = new PerLight[NUMBER_OF_LIGHTS];
		
		public static final int SIZE = 2 * 4 * 4 + NUMBER_OF_LIGHTS * PerLight.SIZE;
		
		public LightBlock(Vector4 ambientIntensity, float lightAttenuation) {
			this.ambientIntensity = ambientIntensity;
			this.lightAttenuation = lightAttenuation;
		}
		
		private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(SIZE / 4);
		
		public FloatBuffer toBuffer() {
			buffer.clear();
			buffer.put(ambientIntensity.toBuffer());
			buffer.put(lightAttenuation);
			buffer.put(new float[3]);
			
			for(PerLight light : lights)
				buffer.put(light.toBuffer());
			
			buffer.flip();
			return buffer;
		}
	}
	
	public static class LightBlockHDR {
		public Vector4 ambientIntensity;
		public float lightAttenuation;
		public float maxIntensity;
		public PerLight[] lights = new PerLight[NUMBER_OF_LIGHTS];
		
		public static final int SIZE = 2 * 4 * 4 + NUMBER_OF_LIGHTS * PerLight.SIZE;
		
		public LightBlockHDR(Vector4 ambientIntensity, float lightAttenuation, float maxIntensity) {
			this.ambientIntensity = ambientIntensity;
			this.lightAttenuation = lightAttenuation;
			this.maxIntensity = maxIntensity;
		}
		
		private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(SIZE / 4);
		
		public FloatBuffer toBuffer() {
			buffer.clear();
			buffer.put(ambientIntensity.toBuffer());
			buffer.put(lightAttenuation);
			buffer.put(maxIntensity);
			buffer.put(new float[2]);
			
			for(PerLight light : lights)
				buffer.put(light.toBuffer());
			
			buffer.flip();
			return buffer;
		}
	}
	
	public static class LightBlockGamma {
		public Vector4 ambientIntensity;
		public float lightAttenuation;
		public float maxIntensity;
		public float gamma;
		public PerLight[] lights = new PerLight[NUMBER_OF_LIGHTS];
		
		public static final int SIZE = 2 * 4 * 4 + NUMBER_OF_LIGHTS * PerLight.SIZE;
		
		public LightBlockGamma(Vector4 ambientIntensity, float lightAttenuation, float maxIntensity, float gamma) {
			this.ambientIntensity = ambientIntensity;
			this.lightAttenuation = lightAttenuation;
			this.maxIntensity = maxIntensity;
			this.gamma = gamma;
		}
		
		private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(SIZE / 4);
		
		public FloatBuffer toBuffer() {
			buffer.clear();
			buffer.put(ambientIntensity.toBuffer());
			buffer.put(lightAttenuation);
			buffer.put(maxIntensity);
			buffer.put(gamma);
			buffer.put(0);
			
			for(PerLight light : lights)
				buffer.put(light.toBuffer());
			
			buffer.flip();
			return buffer;
		}
	}
	
	public static class SunlightValue {
		public float normTime;
		public Vector4 ambient;
		public Vector4 sunlightIntensity;
		public Vector4 backgroundColor;
		
		public SunlightValue(float normTime, Vector4 ambient, Vector4 sunlightIntensity, Vector4 backgroundColor) {
			this.normTime = normTime;
			this.ambient = ambient;
			this.sunlightIntensity = sunlightIntensity;
			this.backgroundColor = backgroundColor;
		}
	}
	
	public static class SunlightValueHDR {
		public float normTime;
		public Vector4 ambient;
		public Vector4 sunlightIntensity;
		public Vector4 backgroundColor;
		public float maxIntensity;
		
		public SunlightValueHDR(float normTime, Vector4 ambient, Vector4 sunlightIntensity, Vector4 backgroundColor, float maxIntensity) {
			this.normTime = normTime;
			this.ambient = ambient;
			this.sunlightIntensity = sunlightIntensity;
			this.backgroundColor = backgroundColor;
			this.maxIntensity = maxIntensity;
		}
	}
	
	public enum TimerTypes {
		TIMER_SUN, TIMER_LIGHTS, TIMER_ALL
	}
}
 public static class LightBlock {
	public Vector4 ambientIntensity;
	public float lightAttenuation;
	public PerLight[] lights = new PerLight[NUMBER_OF_LIGHTS];
		
	public static final int SIZE = 2 * 4 * 4 + NUMBER_OF_LIGHTS * PerLight.SIZE;
		
	public LightBlock(Vector4 ambientIntensity, float lightAttenuation) {
		this.ambientIntensity = ambientIntensity;
		this.lightAttenuation = lightAttenuation;
	}
		
	private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(SIZE / 4);
		
	public FloatBuffer toBuffer() {
		buffer.clear();
		buffer.put(ambientIntensity.toBuffer());
		buffer.put(lightAttenuation);
		buffer.put(new float[3]);
			
		for(PerLight light : lights)
			buffer.put(light.toBuffer());
			
		buffer.flip();
		return buffer;
	}
}
 public static class LightBlockGamma {
	public Vector4 ambientIntensity;
	public float lightAttenuation;
	public float maxIntensity;
	public float gamma;
	public PerLight[] lights = new PerLight[NUMBER_OF_LIGHTS];
		
	public static final int SIZE = 2 * 4 * 4 + NUMBER_OF_LIGHTS * PerLight.SIZE;
		
	public LightBlockGamma(Vector4 ambientIntensity, float lightAttenuation, float maxIntensity, float gamma) {
		this.ambientIntensity = ambientIntensity;
		this.lightAttenuation = lightAttenuation;
		this.maxIntensity = maxIntensity;
		this.gamma = gamma;
	}
		
	private static final FloatBuffer buffer = BufferUtils.createFloatBuffer(SIZE / 4);
		
	public FloatBuffer toBuffer() {
		buffer.clear();
		buffer.put(ambientIntensity.toBuffer());
		buffer.put(lightAttenuation);
		buffer.put(maxIntensity);
		buffer.put(gamma);
		buffer.put(0);
			
		for(PerLight light : lights)
			buffer.put(light.toBuffer());
			
		buffer.flip();
		return buffer;
	}
}
 public static class SunlightValueHDR {
	public float normTime;
	public Vector4 ambient;
	public Vector4 sunlightIntensity;
	public Vector4 backgroundColor;
	public float maxIntensity;
		
	public SunlightValueHDR(float normTime, Vector4 ambient, Vector4 sunlightIntensity, Vector4 backgroundColor, float maxIntensity) {
		this.normTime = normTime;
		this.ambient = ambient;
		this.sunlightIntensity = sunlightIntensity;
		this.backgroundColor = backgroundColor;
		this.maxIntensity = maxIntensity;
	}
}
 public enum TimerTypes {
	TIMER_SUN, TIMER_LIGHTS, TIMER_ALL
}
 public class Scene {
	public final Mesh terrainMesh;
	public final Mesh cubeMesh;
	public final Mesh tetraMesh;
	public final Mesh cylMesh;
	public final Mesh sphereMesh;
	
	private final int sizeMaterialBlock;
	private int materialUniformBuffer;
	
	private ProgramStore store;
	
	private final int MATERIAL_COUNT = 6;
	
	public Scene(ProgramStore store) {
		this.store = store;
		
		try {
			terrainMesh = new Mesh(Scene.class.getResource("example12.Ground.xml"));
			cubeMesh = new Mesh(Scene.class.getResource("example12.UnitCube.xml"));
			tetraMesh = new Mesh(Scene.class.getResource("example12.UnitTetrahedron.xml"));
			cylMesh = new Mesh(Scene.class.getResource("example12.UnitCylinder.xml"));
			sphereMesh = new Mesh(Scene.class.getResource("example12.UnitSphere.xml"));
		} catch(Exception exc) {
			throw new RuntimeException("Error loading meshes", exc);
		}
		
		int uniformBufferAlignSize = glGetInteger(GL_UNIFORM_BUFFER_OFFSET_ALIGNMENT);
		
		sizeMaterialBlock = MaterialBlock.SIZE + uniformBufferAlignSize - (MaterialBlock.SIZE % uniformBufferAlignSize);
		
		int sizeMaterialUniformBuffer = sizeMaterialBlock * MATERIAL_COUNT;
		ArrayList<MaterialBlock> materials = getMaterials();
		
		FloatBuffer buffer = BufferUtils.createByteBuffer(sizeMaterialUniformBuffer).asFloatBuffer();
		for(int a = 0; a < materials.size(); a++) {
			buffer.position(a * sizeMaterialBlock / 4);
			buffer.put(materials.get(a).diffuseColor.toBuffer());
			buffer.put(materials.get(a).specularColor.toBuffer());
			buffer.put(materials.get(a).specularShininess);
		}
		buffer.flip();
		
		materialUniformBuffer = glGenBuffers();
		glBindBuffer(GL_UNIFORM_BUFFER, materialUniformBuffer);
		glBufferData(GL_UNIFORM_BUFFER, buffer, GL_STATIC_DRAW);
		glBindBuffer(GL_UNIFORM_BUFFER, 0);
	}
	
	private ArrayList<MaterialBlock> getMaterials() {
		ArrayList<MaterialBlock> materials = new ArrayList<>(MATERIAL_COUNT);
		materials.add(new MaterialBlock(new Vector4(1), new Vector4(0.5f, 0.5f, 0.5f, 1), 0.6f));
		materials.add(new MaterialBlock(new Vector4(0.5f), new Vector4(0.5f, 0.5f, 0.5f, 1), 0.05f));
		materials.add(new MaterialBlock(new Vector4(0.05f), new Vector4(0.95f, 0.95f, 0.95f, 1), 0.4f));
		materials.add(new MaterialBlock(new Vector4(0.5f), new Vector4(0.3f, 0.3f, 0.3f, 1), 0.1f));
		materials.add(new MaterialBlock(new Vector4(0.5f), new Vector4(0, 0, 0, 1), 0.6f));
		materials.add(new MaterialBlock(new Vector4(0.63f, 0.6f, 0.02f, 1), new Vector4(0.22f, 0.2f, 0, 1), 0.3f));
		return materials;
	}
	
	public void draw(MatrixStack modelMatrix, int materialBlockIndex, float alphaTetra) {
		{
			modelMatrix.pushMatrix();
			modelMatrix.getTop().rotateDeg(-90, 1, 0, 0);
			
			drawObject(terrainMesh, store.getProgram(LightingProgramTypes.VERT_COLOR_DIFFUSE), materialBlockIndex, 0, modelMatrix);
			
			modelMatrix.popMatrix();
		}
		
		{
			modelMatrix.pushMatrix();
			modelMatrix.getTop().translate(75, 5, 75);
			modelMatrix.getTop().rotateDeg(360 * alphaTetra, 0, 1, 0);
			modelMatrix.getTop().scale(10, 10, 10);
			modelMatrix.getTop().translate(0, (float)Math.sqrt(2), 0);
			modelMatrix.getTop().rotateDeg(54.735f, -0.707f, 0, -0.707f);
			
			drawObject(tetraMesh, "lit-color", store.getProgram(LightingProgramTypes.VERT_COLOR_DIFFUSE_SPECULAR), materialBlockIndex, 1, modelMatrix);
			
			modelMatrix.popMatrix();
		}
		
		{
			modelMatrix.pushMatrix();
			modelMatrix.getTop().translate(88, 5, -80);
			modelMatrix.getTop().scale(4, 4, 4);
			modelMatrix.getTop().scale(4, 9, 1);
			modelMatrix.getTop().translate(0, 0.5f, 0);
			
			drawObject(cubeMesh, "lit", store.getProgram(LightingProgramTypes.MTL_COLOR_DIFFUSE_SPECULAR), materialBlockIndex, 2, modelMatrix);
			
			modelMatrix.popMatrix();
		}
		
		{
			modelMatrix.pushMatrix();
			modelMatrix.getTop().translate(-52.5f, 14, 65);
			modelMatrix.getTop().rotateDeg(50, 0, 0, 1);
			modelMatrix.getTop().rotateDeg(-10, 0, 1, 0);
			modelMatrix.getTop().scale(20, 20, 20);
			
			drawObject(cubeMesh, "lit-color", store.getProgram(LightingProgramTypes.VERT_COLOR_DIFFUSE_SPECULAR), materialBlockIndex, 3, modelMatrix);
			
			modelMatrix.popMatrix();
		}
		
		{
			modelMatrix.pushMatrix();
			modelMatrix.getTop().translate(-7, 30, -14);
			modelMatrix.getTop().scale(15, 55, 15);
			modelMatrix.getTop().translate(0, 0.5f, 0);
			
			drawObject(cylMesh, "lit-color", store.getProgram(LightingProgramTypes.VERT_COLOR_DIFFUSE_SPECULAR), materialBlockIndex, 4, modelMatrix);
			
			modelMatrix.popMatrix();
		}
		
		{
			modelMatrix.pushMatrix();
			modelMatrix.getTop().translate(-83, 14, -77);
			modelMatrix.getTop().scale(20, 20, 20);
			
			drawObject(sphereMesh, "lit", store.getProgram(LightingProgramTypes.MTL_COLOR_DIFFUSE_SPECULAR), materialBlockIndex, 5, modelMatrix);
			
			modelMatrix.popMatrix();
		}
	}
	
	public void drawObject(Mesh mesh, ProgramData prog, int materialBlockIndex, int materialIndex, MatrixStack modelMatrix) {
		glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer, materialIndex * sizeMaterialBlock, MaterialBlock.SIZE);
		
		prog.program.begin();
		glUniformMatrix4(prog.modelToCameraMatrixUniform, false, modelMatrix.getTop().toBuffer());
		glUniformMatrix3(prog.normalModelTocameraMatrixUniform, false, new Matrix3(modelMatrix.getTop()).inverse().transpose().toBuffer());
		mesh.render();
		prog.program.end();
		
		glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);
	}
	
	public void drawObject(Mesh mesh, String meshName, ProgramData prog, int materialBlockIndex, int materialIndex, MatrixStack modelMatrix) {
		glBindBufferRange(GL_UNIFORM_BUFFER, materialBlockIndex, materialUniformBuffer, materialIndex * sizeMaterialBlock, MaterialBlock.SIZE);
		
		prog.program.begin();
		glUniformMatrix4(prog.modelToCameraMatrixUniform, false, modelMatrix.getTop().toBuffer());
		glUniformMatrix3(prog.normalModelTocameraMatrixUniform, false, new Matrix3(modelMatrix.getTop()).inverse().transpose().toBuffer());
		mesh.render(meshName);
		prog.program.end();
		
		glBindBufferBase(GL_UNIFORM_BUFFER, materialBlockIndex, 0);
	}
	
	public static interface ProgramStore {
		public ProgramData getProgram(LightingProgramTypes type);
	}
	
	public static enum LightingProgramTypes {
		VERT_COLOR_DIFFUSE_SPECULAR,
		VERT_COLOR_DIFFUSE,
		
		MTL_COLOR_DIFFUSE_SPECULAR,
		MTL_COLOR_DIFFUSE
	}
	
	public static class ProgramData {
		public ShaderProgram program;
		
		public int modelToCameraMatrixUniform;
		public int normalModelTocameraMatrixUniform;
		
		public ProgramData(ShaderProgram program) {
			this.program = program;
		}
	}
	
	public static class MaterialBlock {
		public static final int SIZE = 3 * 4 * 4;
		
		public Vector4 diffuseColor;
		public Vector4 specularColor;
		public float specularShininess;
		
		public MaterialBlock(Vector4 diffuseColor, Vector4 specularColor, float specularShininess) {
			this.diffuseColor = diffuseColor;
			this.specularColor = specularColor;
			this.specularShininess = specularShininess;
		}
	}
}
 public static enum LightingProgramTypes {
	VERT_COLOR_DIFFUSE_SPECULAR,
	VERT_COLOR_DIFFUSE,
		
	MTL_COLOR_DIFFUSE_SPECULAR,
	MTL_COLOR_DIFFUSE
}
 public static class ProgramData {
	public ShaderProgram program;
		
	public int modelToCameraMatrixUniform;
	public int normalModelTocameraMatrixUniform;
		
	public ProgramData(ShaderProgram program) {
		this.program = program;
	}
}
 public static interface ProgramStore {
	public ProgramData getProgram(LightingProgramTypes type);
}
 public enum MouseButton {
	LEFT_BUTTON,
	MIDDLE_BUTTON,
	RIGHT_BUTTON;
		
	public static MouseButton getButton(int button) {
		switch(button) {
			case 0:
				return MouseButton.LEFT_BUTTON;
			case 1:
				return MouseButton.RIGHT_BUTTON;
			case 2:
				return MouseButton.MIDDLE_BUTTON;
			case -1:
				return null;
			default:
				throw new IllegalArgumentException("Invalid button: " + button);
		}
	}
}
 public static class ViewData {
	public Vector3 targetPos;
	public Quaternion orient;
	public float radius;
	public float degSpinRotation;
		
	public ViewData(ViewData data) {
		targetPos = data.targetPos.copy();
		orient = data.orient.copy();
		radius = data.radius;
		degSpinRotation = data.degSpinRotation;
	}
		
	public ViewData(Vector3 v, Quaternion q, float r, float d) {
		targetPos = v;
		orient = q.copy();
		radius = r;
		degSpinRotation = d;
	}
}
 public static class ViewPole extends ViewProvider {
	private enum TargetOffsetDir {
		DIR_UP,
		DIR_DOWN,
		DIR_FORWARD,
		DIR_BACKWARD,
		DIR_RIGHT,
		DIR_LEFT
	}
		
	private enum RotateMode {
		DUAL_AXIS_ROTATE,
		BIAXIAL_ROTATE,
		XZ_AXIS_ROTATE,
		Y_AXIS_ROTATE,
		SPIN_VIEW_AXIS
	}
		
	private Vector3[] offsets = {
			new Vector3(0, 1, 0),
			new Vector3(0, -1, 0),
			new Vector3(0, 0, -1),
			new Vector3(0, 0, 1),
			new Vector3(1, 0, 0),
			new Vector3(-1, 0, 0)
	};
		
	private ViewData currView;
	private ViewScale viewScale;
		
	private ViewData initialView;
	private MouseButton actionButton;
	private boolean rightKeyboardCtrls;
		
	private boolean isDragging;
	private RotateMode rotateMode;
		
	private float degStarDragSpin;
	private Vector2 startDragMouseLoc;
	private Quaternion startDragOrient;
		
	public ViewPole(ViewData initialView, ViewScale viewScale, MouseButton actionButton) {
		this(initialView, viewScale, actionButton, false);
	}
		
	public ViewPole(ViewData initialView, ViewScale viewScale, MouseButton actionButton, boolean rightKeyboardCtrls) {
		this.currView = new ViewData(initialView);
		this.viewScale = new ViewScale(viewScale);
		this.initialView = initialView;
		this.actionButton = actionButton;
		this.rightKeyboardCtrls = rightKeyboardCtrls;
	}
		
	@Override
	public Matrix4 calcMatrix() {
		Matrix4 mat = new Matrix4().clearToIdentity();
			
		mat.translate(0, 0, -currView.radius);
			
		Quaternion fullRotation = Utils.angleAxisDeg(currView.degSpinRotation, new Vector3(0, 0, 1)).mult(currView.orient);
		mat.mult(fullRotation.toMatrix());
			
		mat.translate(currView.targetPos.copy().mult(-1));
			
		return mat;
	}
		
	public void reset() {
		if(!isDragging)
			currView = new ViewData(initialView);
	}
		
	public void setRotationScale(float rotateScale) {
		viewScale.rotationScale = rotateScale;
	}
		
	public float getRotationScale() {
		return viewScale.rotationScale;
	}
		
	public ViewData getView() {
		return currView;
	}
		
	public boolean isDragging() {
		return isDragging;
	}
		
	public void processXChange(int diffX) {
		float degAngleDiff = diffX * viewScale.rotationScale;
			
		currView.orient = startDragOrient.copy().mult(Utils.angleAxisDeg(degAngleDiff, new Vector3(0, 1, 0)));
	}
		
	public void processYChange(int diffY) {
		float degAngleDiff = diffY * viewScale.rotationScale;
			
		currView.orient = Utils.angleAxisDeg(degAngleDiff, new Vector3(1, 0, 0)).mult(startDragOrient);
	}
		
	public void processXYChange(int diffX, int diffY) {
		float degXAngleDiff = diffX * viewScale.rotationScale;
		float degYAngleDiff = diffY * viewScale.rotationScale;
			
		currView.orient = startDragOrient.copy().mult(Utils.angleAxisDeg(degXAngleDiff, new Vector3(0, 1, 0)));
		currView.orient = Utils.angleAxisDeg(degYAngleDiff, new Vector3(1, 0, 0)).mult(currView.orient);
	}
		
	public void processSpinAxis(int diffX) {
		float degSpinDiff = diffX * viewScale.rotationScale;
		currView.degSpinRotation = degSpinDiff + degStarDragSpin;
	}
		
	public void beginDragRotate(Vector2 start, RotateMode rotMode) {
		rotateMode = rotMode;
			
		startDragMouseLoc = start.copy();
			
		degStarDragSpin = currView.degSpinRotation;
			
		startDragOrient = currView.orient.copy();
			
		isDragging = true;
	}
		
	public void onDragRotate(Vector2 position) {
		int diffX = (int)(position.x() - startDragMouseLoc.x());
		int diffY = -(int)(position.y() - startDragMouseLoc.y());
			
		switch(rotateMode) {
			case DUAL_AXIS_ROTATE:
				processXYChange(diffX, diffY);
				break;
			case BIAXIAL_ROTATE:
				if(Math.abs(diffX) > Math.abs(diffY))
					processXChange(diffX);
				else
					processYChange(diffY);
				break;
			case XZ_AXIS_ROTATE:
				processXChange(diffX);
				break;
			case Y_AXIS_ROTATE:
				processYChange(diffY);
				break;
			case SPIN_VIEW_AXIS:
				processSpinAxis(diffX);
				break;
		}
	}
		
	public void endDragRotate(Vector2 end, boolean keepResults) {
		if(keepResults)
			onDragRotate(end);
		else
			currView.orient = startDragOrient.copy();
			
		isDragging = false;
	}
		
	public void moveCloser(boolean largeStep) {
		if(largeStep)
			currView.radius -= viewScale.largeRadiusDelta;
		else
			currView.radius -= viewScale.smallRadiusDelta;
			
		if(currView.radius < viewScale.minRadius)
			currView.radius = viewScale.minRadius;
	}
		
	public void moveAway(boolean largeStep) {
		if(largeStep)
			currView.radius += viewScale.largeRadiusDelta;
		else
			currView.radius += viewScale.smallRadiusDelta;
			
		if(currView.radius > viewScale.maxRadius)
			currView.radius = viewScale.maxRadius;
	}
		
	@Override
	public void mouseMove(Vector2 position) {
		if(isDragging)
			onDragRotate(position);
	}
		
	@Override
	public void mouseClick(MouseButton button, boolean isPressed, MouseModifier modifiers, Vector2 position) {
		if(isPressed) {
			if(!isDragging) {
				if(button == actionButton) {
					if(modifiers == MouseModifier.KEY_CTRL)
						beginDragRotate(position, RotateMode.BIAXIAL_ROTATE);
					else if(modifiers == MouseModifier.KEY_ALT)
						beginDragRotate(position, RotateMode.SPIN_VIEW_AXIS);
					else
						beginDragRotate(position, RotateMode.DUAL_AXIS_ROTATE);
				}
			}
		} else {
			if(isDragging) {
				if(button == actionButton) {
					if(rotateMode == RotateMode.DUAL_AXIS_ROTATE ||
							rotateMode == RotateMode.SPIN_VIEW_AXIS ||
							rotateMode == RotateMode.BIAXIAL_ROTATE)
						endDragRotate(position, true);
				}
			}
		}
	}
		
	@Override
	public void mouseWheel(int direction, MouseModifier modifiers) {
		if(direction > 0)
			moveCloser(modifiers != MouseModifier.KEY_SHIFT);
		else
			moveAway(modifiers != MouseModifier.KEY_SHIFT);
	}
		
	private void offsetTargetPos(TargetOffsetDir dir, float worldDistance) {
		offsetTargetPos(offsets[dir.ordinal()].copy().mult(worldDistance));
	}
		
	private void offsetTargetPos(Vector3 cameraOffset) {
		currView.targetPos.add(calcMatrix().toQuaternion().conjugate().mult(cameraOffset));
	}
		
	@Override
	public void charPress(long deltaTime) {
		boolean isShiftPressed = Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT);
			
		float offset = (isShiftPressed ? viewScale.smallPosOffset : viewScale.largePosOffset) * 10 * deltaTime / (float)1e9;
			
		if(rightKeyboardCtrls) {
			if(Keyboard.isKeyDown(Keyboard.KEY_I))
				offsetTargetPos(TargetOffsetDir.DIR_FORWARD, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_K))
				offsetTargetPos(TargetOffsetDir.DIR_BACKWARD, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_L))
				offsetTargetPos(TargetOffsetDir.DIR_RIGHT, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_J))
				offsetTargetPos(TargetOffsetDir.DIR_LEFT, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_O))
				offsetTargetPos(TargetOffsetDir.DIR_UP, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_U))
				offsetTargetPos(TargetOffsetDir.DIR_DOWN, offset);
		}
		else {
			if(Keyboard.isKeyDown(Keyboard.KEY_W))
				offsetTargetPos(TargetOffsetDir.DIR_FORWARD, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_S))
				offsetTargetPos(TargetOffsetDir.DIR_BACKWARD, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_D))
				offsetTargetPos(TargetOffsetDir.DIR_RIGHT, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_A))
				offsetTargetPos(TargetOffsetDir.DIR_LEFT, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_E))
				offsetTargetPos(TargetOffsetDir.DIR_UP, offset);
			if(Keyboard.isKeyDown(Keyboard.KEY_Q))
				offsetTargetPos(TargetOffsetDir.DIR_DOWN, offset);
		}
	}
}
 public static class ViewScale {
	public float minRadius;
	public float maxRadius;
	public float largeRadiusDelta;
	public float smallRadiusDelta;
	public float largePosOffset;
	public float smallPosOffset;
	public float rotationScale;
		
	public ViewScale(float min, float max, float large, float small, float largePos, float smallPos, float rot) {
		minRadius = min;
		maxRadius = max;
		largeRadiusDelta = large;
		smallRadiusDelta = small;
		largePosOffset = largePos;
		smallPosOffset = smallPos;
		rotationScale = rot;
	}
		
	public ViewScale(ViewScale v) {
		minRadius = v.minRadius;
		maxRadius = v.maxRadius;
		largeRadiusDelta = v.largeRadiusDelta;
		smallRadiusDelta = v.smallRadiusDelta;
		largePosOffset = v.largePosOffset;
		smallPosOffset = v.smallPosOffset;
		rotationScale = v.rotationScale;
	}
}
 public class ShaderProgram {
	private int program;
	
	public ShaderProgram(String vertexShader, String fragmentShader) {
		this(vertexShader, null, fragmentShader, null);
	}
	
	public ShaderProgram(String vertexShader, String fragmentShader, Map<Integer,String> attributes) {
		this(vertexShader, null, fragmentShader, attributes);
	}
	
	public ShaderProgram(String vertexShader, String geometryShader, String fragmentShader) {
		this(vertexShader, geometryShader, fragmentShader, null);
	}
	
	public ShaderProgram(String vertexShader, String geometryShader, String fragmentShader, Map<Integer,String> attributes) {
		int vs = compileShader(vertexShader, GL_VERTEX_SHADER);
		int gs = compileShader(geometryShader, GL_GEOMETRY_SHADER);
		int fs = compileShader(fragmentShader, GL_FRAGMENT_SHADER);
		
		program = glCreateProgram();
		glAttachShader(program, vs);
		if(gs != -1)
			glAttachShader(program, gs);
		glAttachShader(program, fs);
		
		if(attributes != null)
			for(int i : attributes.keySet())
				glBindAttribLocation(program, i, attributes.get(i));
		
		glLinkProgram(program);
		
		String infoLog = glGetProgramInfoLog(program, glGetProgrami(program, GL_INFO_LOG_LENGTH));
		
		if(glGetProgrami(program, GL_LINK_STATUS) == GL_FALSE)
			throw new RuntimeException("Failure in linking program. Error log:\n" + infoLog);
		else {
			System.out.print("Linking program successful.");
			if(infoLog != null && !(infoLog = infoLog.trim()).isEmpty())
				System.out.println(" Log:\n" + infoLog);
			else
				System.out.println();
		}
		
		glDetachShader(program, vs);
		if(gs != -1)
			glDetachShader(program, gs);
		glDetachShader(program, fs);
		
		glDeleteShader(vs);
		if(gs != -1)
			glDeleteShader(gs);
		glDeleteShader(fs);
	}
	
	private int compileShader(String source, int type) {
		if(source == null)
			return -1;
		
		int shader = glCreateShader(type);
		glShaderSource(shader, source);
		
		glCompileShader(shader);
		
		String infoLog = glGetShaderInfoLog(shader, glGetShaderi(shader, GL_INFO_LOG_LENGTH));
		
		if(glGetShaderi(shader, GL_COMPILE_STATUS) == GL_FALSE)
			throw new RuntimeException("Failure in compiling " + getName(type) + " shader. Error log:\n" + infoLog);
		else {
			System.out.print("Compiling " + getName(type) + " shader successful.");
			if(infoLog != null && !(infoLog = infoLog.trim()).isEmpty())
				System.out.println(" Log:\n" + infoLog);
			else
				System.out.println();
		}
		
		return shader;
	}
	
	private String getName(int shaderType) {
		if(shaderType == GL_VERTEX_SHADER)
			return "vertex";
		if(shaderType == GL_GEOMETRY_SHADER)
			return "geometry";
		if(shaderType == GL_FRAGMENT_SHADER)
			return "fragment";
		
		throw new IllegalArgumentException("Invalid shaderType, must be either GL_VERTEX_SHADER, GL_GEOMETRY_SHADER, or GL_FRAGMENT_SHADER");
	}
	
	public int getProgram() {
		return program;
	}
	
	public int getUniformLocation(String name) {
		return glGetUniformLocation(program, name);
	}
	
	public int getUniformBlockIndex(String name) {
		return glGetUniformBlockIndex(program, name);
	}
	
	public void begin() {
		glUseProgram(program);
	}
	
	public void end() {
		glUseProgram(0);
	}
	
	public void destroy() {
		glDeleteProgram(program);
	}
}
 public enum Type {
	LOOP,
	SINGLE,
	INFINITE
}
 public class Utils {
	public static boolean ENABLE_ERROR_CHECKING = true;
	
	public static void checkGLError(String event) {
		int error;
		if(ENABLE_ERROR_CHECKING && (error = glGetError()) != GL_NO_ERROR)
			throw new RuntimeException("OpenGL Error during " + event + ": " + gluErrorString(error));
	}
	
	public static Quaternion angleAxisDeg(float angle, Vector3 vec) {
		return new Quaternion((float)Math.toRadians(angle), vec);
	}
	
	public static float clamp(float value, float low, float high) {
		return Math.min(Math.max(value, low), high);
	}
	
	public static float mix(float f1, float f2, float a) {
		return f1 + (f2 - f1) * a;
	}
	
	public static Matrix4 lookAt(Vector3 eye, Vector3 center, Vector3 up) {
		Vector3 f = center.copy().sub(eye).normalize();
		up = up.copy().normalize();
		
		Vector3 s = f.cross(up);
		Vector3 u = s.cross(f);
		
		return new Matrix4(new float[] {
				s.x(), u.x(), -f.x(), 0,
				s.y(), u.y(), -f.y(), 0,
				s.z(), u.z(), -f.z(), 0,
				0, 0, 0, 1
		}).translate(eye.copy().mult(-1));
	}
	
	public static void updateMousePoles(ViewPole... viewPoles) {
		updateMousePoles(viewPoles, null);
	}
	
	public static void updateMousePoles(ObjectPole... objectPoles) {
		updateMousePoles(null, objectPoles);
	}
	
	public static void updateMousePoles(ViewPole viewPole, ObjectPole objectPole) {
		updateMousePoles(viewPole == null ? null : new ViewPole[] { viewPole }, objectPole == null ? null : new ObjectPole[] { objectPole });
	}
	
	public static void updateMousePoles(ViewPole[] viewPoles, ObjectPole[] objectPoles) {
		while(Mouse.next()) {
			MouseButton button = MouseButton.getButton(Mouse.getEventButton());
			if(button != null) {
				boolean pressed = Mouse.getEventButtonState();
				if(viewPoles != null)
					for(ViewPole v : viewPoles)
						v.mouseClick(button, pressed, getModifier(), new Vector2(Mouse.getX(), Mouse.getY()));
				if(objectPoles != null)
					for(ObjectPole o : objectPoles)
						o.mouseClick(button, pressed, getModifier(), new Vector2(Mouse.getX(), Mouse.getY()));
			} else {
				int dwheel = Mouse.getDWheel();
				
				if(dwheel != 0) {
					if(viewPoles != null)
						for(ViewPole v : viewPoles)
							v.mouseWheel(dwheel, getModifier());
					if(objectPoles != null)
						for(ObjectPole o : objectPoles)
							o.mouseWheel(dwheel, getModifier());
				} else {
					if(viewPoles != null)
						for(ViewPole v : viewPoles)
							v.mouseMove(new Vector2(Mouse.getX(), Mouse.getY()));
					if(objectPoles != null)
						for(ObjectPole o : objectPoles)
							o.mouseMove(new Vector2(Mouse.getX(), Mouse.getY()));
				}
			}
		}
	}
	
	private static MouseModifier getModifier() {
		if(Keyboard.isKeyDown(Keyboard.KEY_LSHIFT) || Keyboard.isKeyDown(Keyboard.KEY_RSHIFT))
			return MouseModifier.KEY_SHIFT;
		
		if(Keyboard.isKeyDown(Keyboard.KEY_LCONTROL) || Keyboard.isKeyDown(Keyboard.KEY_RCONTROL))
			return MouseModifier.KEY_CTRL;
		
		if(Keyboard.isKeyDown(Keyboard.KEY_LMENU))
			return MouseModifier.KEY_ALT;
		
		return null;
	}
	
	public static String readFully(InputStream is) {
		try(BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) {
			StringBuilder s = new StringBuilder();
			String l;
			
			while((l = reader.readLine()) != null)
				s.append(l).append('\n');
			
			return s.toString();
		} catch(Exception exc) {
			throw new RuntimeException("Failure reading input stream", exc);
		}
	}
	
	public static Quaternion parseQuaternion(String s) {
		String[] comp = StringUtil.split(s, ' ');
		if(comp.length != 4)
			throw new IllegalArgumentException("invalid Quaternion");
		
		Quaternion quat = new Quaternion();
		quat.x(Float.parseFloat(comp[0]));
		quat.y(Float.parseFloat(comp[1]));
		quat.z(Float.parseFloat(comp[2]));
		quat.w(Float.parseFloat(comp[3]));
		
		return quat;
	}
	
	public static Vector4 parseVector4(String s) {
		String[] comp = StringUtil.split(s, ' ');
		if(comp.length != 4)
			throw new IllegalArgumentException("invalid Vector4");
		
		Vector4 vec = new Vector4();
		vec.x(Float.parseFloat(comp[0]));
		vec.y(Float.parseFloat(comp[1]));
		vec.z(Float.parseFloat(comp[2]));
		vec.w(Float.parseFloat(comp[3]));
		
		return vec;
	}
	
	public static Vector3 parseVector3(String s) throws NumberFormatException {
		String[] comp = StringUtil.split(s, ' ');
		if(comp.length != 3)
			throw new IllegalArgumentException("invalid Vector3");
		
		Vector3 vec = new Vector3();
		vec.x(Float.parseFloat(comp[0]));
		vec.y(Float.parseFloat(comp[1]));
		vec.z(Float.parseFloat(comp[2]));
		
		return vec;
	}
	
	public static Vector2 parseVector2(String s) throws NumberFormatException {
		String[] comp = StringUtil.split(s, ' ');
		if(comp.length != 2)
			throw new IllegalArgumentException("invalid Vector2");
		
		Vector2 vec = new Vector2();
		vec.x(Float.parseFloat(comp[0]));
		vec.y(Float.parseFloat(comp[1]));
		
		return vec;
	}
}
 public class Matrix4 {
	private float[] matrix;
	
	public Matrix4() {
		matrix = new float[16];
	}
	
	public Matrix4(float[] m) {
		this();
		set(m);
	}
	
	public Matrix4(Matrix3 m) {
		this();
		set(m);
	}
	
	public Matrix4(Matrix4 m) {
		this();
		set(m);
	}
	
	public Matrix4 copy() {
		return new Matrix4(this);
	}
	
	public Matrix4 clear() {
		Arrays.fill(matrix, 0);
		return this;
	}
	
	public Matrix4 clearToIdentity() {
		return clear().put(0, 1)
				.put(5, 1)
				.put(10, 1)
				.put(15, 1);
	}
	
	public Matrix4 clearToOrtho(float left, float right, float bottom, float top, float near, float far) {
		return clear().put(0, 2 / (right - left))
				.put(5, 2 / (top - bottom))
				.put(10, -2 / (far - near))
				.put(12, -(right + left) / (right - left))
				.put(13, -(top + bottom) / (top - bottom))
				.put(14, -(far + near) / (far - near))
				.put(15, 1);
	}
	
	public Matrix4 clearToPerspective(float fovRad, float width, float height, float near, float far) {
		float fov = 1 / (float)Math.tan(fovRad / 2);
		return clear().put(0, fov / (width / height))
				.put(5, fov).put(10, (far + near) / (near - far))
				.put(14, (2 * far * near) / (near - far))
				.put(11, -1);
	}
	
	public Matrix4 clearToPerspectiveDeg(float fov, float width, float height, float near, float far) {
		return clearToPerspective((float)Math.toRadians(fov), width, height, near, far);
	}
	
	public float get(int index) {
		return matrix[index];
	}
	
	public Vector4 getColumn(int index) {
		return new Vector4(get(index * 4 + 0), get(index * 4 + 1), get(index * 4 + 2), get(index * 4 + 3));
	}
	
	public Matrix4 put(int index, float f) {
		matrix[index] = f;
		return this;
	}
	
	public Matrix4 putColumn(int index, Vector4 v) {
		put(index * 4 + 0, v.x());
		put(index * 4 + 1, v.y());
		put(index * 4 + 2, v.z());
		put(index * 4 + 3, v.z());
		return this;
	}
	
	public Matrix4 putColumn(int index, Vector3 v) {
		put(index * 4 + 0, v.x());
		put(index * 4 + 1, v.y());
		put(index * 4 + 2, v.z());
		return this;
	}
	
	public Matrix4 putColumn(int index, Vector3 v, float w) {
		put(index * 4 + 0, v.x());
		put(index * 4 + 1, v.y());
		put(index * 4 + 2, v.z());
		put(index * 4 + 3, w);
		return this;
	}
	
	public Matrix4 set(float[] m) {
		if(m.length < matrix.length)
			throw new IllegalArgumentException("float array must have at least " + matrix.length + " values.");
		
		System.arraycopy(m, 0, matrix, 0, matrix.length);
		
		return this;
	}
	
	public Matrix4 set(Matrix4 m) {
		return set(m.matrix);
	}
	
	public Matrix4 set(Matrix3 m) {
		for(int a = 0; a < 3; a++) {
			matrix[a * 4 + 0] = m.get(a * 3 + 0);
			matrix[a * 4 + 1] = m.get(a * 3 + 1);
			matrix[a * 4 + 2] = m.get(a * 3 + 2);
		}
		
		return this;
	}
	
	public Matrix4 mult(float f) {
		for(int a = 0; a < matrix.length; a++)
			put(a, get(a) * f);
		
		return this;
	}
	
	public Matrix4 mult(float[] m) {
		float[] newm = new float[matrix.length];
		
		for(int a = 0; a < matrix.length; a += 4) {
			newm[a + 0] = get(0) * m[a] + get(4) * m[a + 1] + get(8) * m[a + 2] + get(12) * m[a + 3];
			newm[a + 1] = get(1) * m[a] + get(5) * m[a + 1] + get(9) * m[a + 2] + get(13) * m[a + 3];
			newm[a + 2] = get(2) * m[a] + get(6) * m[a + 1] + get(10) * m[a + 2] + get(14) * m[a + 3];
			newm[a + 3] = get(3) * m[a] + get(7) * m[a + 1] + get(11) * m[a + 2] + get(15) * m[a + 3];
		}
		
		set(newm);
		
		return this;
	}
	
	public Matrix4 mult(Matrix4 m) {
		return mult(m.matrix);
	}
	
	public Vector4 mult(Vector4 vec) {
		Vector4 v = new Vector4();
		
		v.x(get(0) * vec.x() + get(4) * vec.y() + get(8) * vec.z() + get(12) * vec.w());
		v.y(get(1) * vec.x() + get(5) * vec.y() + get(9) * vec.z() + get(13) * vec.w());
		v.z(get(2) * vec.x() + get(6) * vec.y() + get(10) * vec.z() + get(14) * vec.w());
		v.w(get(3) * vec.x() + get(7) * vec.y() + get(11) * vec.z() + get(15) * vec.w());
		
		return v;
	}
	
	public Matrix4 transpose() {
		float old = get(1);
		put(1, get(4));
		put(4, old);
		
		old = get(2);
		put(2, get(8));
		put(8, old);
		
		old = get(3);
		put(3, get(12));
		put(12, old);
		
		old = get(7);
		put(7, get(13));
		put(13, old);
		
		old = get(11);
		put(11, get(14));
		put(14, old);
		
		old = get(6);
		put(6, get(9));
		put(9, old);
		
		return this;
	}
	
	public Matrix4 translate(float x, float y, float z) {
		float[] m = new float[matrix.length];
		
		m[0] = 1;
		m[5] = 1;
		m[10] = 1;
		m[15] = 1;
		
		m[12] = x;
		m[13] = y;
		m[14] = z;
		
		return mult(m);
	}
	
	public Matrix4 translate(Vector3 vec) {
		return translate(vec.x(), vec.y(), vec.z());
	}
	
	public Matrix4 scale(float f) {
		return scale(f, f, f);
	}
	
	public Matrix4 scale(float x, float y, float z) {
		float[] m = new float[matrix.length];
		
		m[0] = x;
		m[5] = y;
		m[10] = z;
		m[15] = 1;
		
		return mult(m);
	}
	
	public Matrix4 scale(Vector3 vec) {
		return scale(vec.x(), vec.y(), vec.z());
	}
	
	public Matrix4 rotate(float angle, float x, float y, float z) {
		float cos = (float)Math.cos(angle);
		float sin = (float)Math.sin(angle);
		float oneMinusCos = 1 - cos;
		
		Vector3 v = new Vector3(x, y, z).normalize();
		
		float[] m = new float[matrix.length];
		m[0] = v.x() * v.x() + (1 - v.x() * v.x()) * cos;
		m[4] = v.x() * v.y() * oneMinusCos - v.z() * sin;
		m[8] = v.x() * v.z() * oneMinusCos + v.y() * sin;
		
		m[1] = v.y() * v.x() * oneMinusCos + v.z() * sin;
		m[5] = v.y() * v.y() + (1 - v.y() * v.y()) * cos;
		m[9] = v.y() * v.z() * oneMinusCos - v.x() * sin;
		
		m[2] = v.z() * v.x() * oneMinusCos - v.y() * sin;
		m[6] = v.z() * v.y() * oneMinusCos + v.x() * sin;
		m[10] = v.z() * v.z() + (1 - v.z() * v.z()) * cos;
		
		m[15] = 1;
		
		return mult(m);
	}
	
	public Matrix4 rotate(float angle, Vector3 vec) {
		return rotate(angle, vec.x(), vec.y(), vec.z());
	}
	
	public Matrix4 rotateDeg(float angle, float x, float y, float z) {
		return rotate((float)Math.toRadians(angle), x, y, z);
	}
	
	public Matrix4 rotateDeg(float angle, Vector3 vec) {
		return rotate((float)Math.toRadians(angle), vec);
	}
	
	public float determinant() {
		float a = get(5) * get(10) * get(15) + get(9) * get(14) * get(7) + get(13) * get(6) * get(11) - get(7) * get(10) * get(13) - get(11) * get(14) * get(5) - get(15) * get(6) * get(9);
		float b = get(1) * get(10) * get(15) + get(9) * get(14) * get(3) + get(13) * get(2) * get(11) - get(3) * get(10) * get(13) - get(11) * get(14) * get(1) - get(15) * get(2) * get(9);
		float c = get(1) * get(6) * get(15) + get(5) * get(14) * get(3) + get(13) * get(2) * get(7) - get(3) * get(6) * get(13) - get(7) * get(14) * get(1) - get(15) * get(2) * get(5);
		float d = get(1) * get(6) * get(11) + get(5) * get(10) * get(3) + get(9) * get(2) * get(7) - get(3) * get(6) * get(9) - get(7) * get(10) * get(1) - get(11) * get(2) * get(5);
		
		return get(0) * a - get(4) * b + get(8) * c - get(12) * d;
	}
	
	public Matrix4 inverse() {
		Matrix4 inv = new Matrix4();
		
		inv.put(0, +(get(5) * get(10) * get(15) + get(9) * get(14) * get(7) + get(13) * get(6) * get(11) - get(7) * get(10) * get(13) - get(11) * get(14) * get(5) - get(15) * get(6) * get(9)));
		inv.put(1, -(get(4) * get(10) * get(15) + get(8) * get(14) * get(7) + get(12) * get(6) * get(11) - get(7) * get(10) * get(12) - get(11) * get(14) * get(4) - get(15) * get(6) * get(8)));
		inv.put(2, +(get(4) * get(9) * get(15) + get(8) * get(13) * get(7) + get(12) * get(5) * get(11) - get(7) * get(9) * get(12) - get(11) * get(13) * get(4) - get(15) * get(5) * get(8)));
		inv.put(3, -(get(4) * get(9) * get(14) + get(8) * get(13) * get(6) + get(12) * get(5) * get(10) - get(6) * get(9) * get(12) - get(10) * get(13) * get(4) - get(14) * get(5) * get(8)));
		
		inv.put(4, -(get(1) * get(10) * get(15) + get(9) * get(14) * get(3) + get(13) * get(2) * get(11) - get(3) * get(10) * get(13) - get(11) * get(14) * get(1) - get(15) * get(2) * get(9)));
		inv.put(5, +(get(0) * get(10) * get(15) + get(8) * get(14) * get(3) + get(12) * get(2) * get(11) - get(3) * get(10) * get(12) - get(11) * get(14) * get(0) - get(15) * get(2) * get(8)));
		inv.put(6, -(get(0) * get(9) * get(15) + get(8) * get(13) * get(3) + get(12) * get(1) * get(11) - get(3) * get(9) * get(12) - get(11) * get(13) * get(0) - get(15) * get(1) * get(8)));
		inv.put(7, +(get(0) * get(9) * get(14) + get(8) * get(13) * get(2) + get(12) * get(1) * get(10) - get(2) * get(9) * get(12) - get(10) * get(13) * get(0) - get(14) * get(1) * get(8)));
		
		inv.put(8, +(get(1) * get(6) * get(15) + get(5) * get(14) * get(3) + get(13) * get(2) * get(7) - get(3) * get(6) * get(13) - get(7) * get(14) * get(1) - get(15) * get(2) * get(5)));
		inv.put(9, -(get(0) * get(6) * get(15) + get(4) * get(14) * get(3) + get(12) * get(2) * get(7) - get(3) * get(6) * get(12) - get(7) * get(14) * get(0) - get(15) * get(2) * get(4)));
		inv.put(10, +(get(0) * get(5) * get(15) + get(4) * get(13) * get(3) + get(12) * get(1) * get(7) - get(3) * get(5) * get(12) - get(7) * get(13) * get(0) - get(15) * get(1) * get(4)));
		inv.put(11, -(get(0) * get(5) * get(14) + get(4) * get(13) * get(2) + get(12) * get(1) * get(6) - get(2) * get(5) * get(12) - get(6) * get(13) * get(0) - get(14) * get(1) * get(4)));
		
		inv.put(12, -(get(1) * get(6) * get(11) + get(5) * get(10) * get(3) + get(9) * get(2) * get(7) - get(3) * get(6) * get(9) - get(7) * get(10) * get(1) - get(11) * get(2) * get(5)));
		inv.put(13, +(get(0) * get(6) * get(11) + get(4) * get(10) * get(3) + get(8) * get(2) * get(7) - get(3) * get(6) * get(8) - get(7) * get(10) * get(0) - get(11) * get(2) * get(4)));
		inv.put(14, -(get(0) * get(5) * get(11) + get(4) * get(9) * get(3) + get(8) * get(1) * get(7) - get(3) * get(5) * get(8) - get(7) * get(9) * get(0) - get(11) * get(1) * get(4)));
		inv.put(15, +(get(0) * get(5) * get(10) + get(4) * get(9) * get(2) + get(8) * get(1) * get(6) - get(2) * get(5) * get(8) - get(6) * get(9) * get(0) - get(10) * get(1) * get(4)));
		
		return set(inv.transpose().mult(1 / determinant()));
	}
	
	public Quaternion toQuaternion() {
		float x = get(0) - get(5) - get(10);
		float y = get(5) - get(0) - get(10);
		float z = get(10) - get(0) - get(5);
		float w = get(0) + get(5) + get(10);
		
		int biggestIndex = 0;
		float biggest = w;
		
		if(x > biggest) {
			biggest = x;
			biggestIndex = 1;
		}
		
		if(y > biggest) {
			biggest = y;
			biggestIndex = 2;
		}
		
		if(z > biggest) {
			biggest = z;
			biggestIndex = 3;
		}
		
		float biggestVal = (float)(Math.sqrt(biggest + 1) * 0.5);
		float mult = 0.25f / biggestVal;
		
		Quaternion res = new Quaternion();
		
		switch(biggestIndex) {
			case 0:
				res.w(biggestVal);
				res.x((get(6) - get(9)) * mult);
				res.y((get(8) - get(2)) * mult);
				res.z((get(1) - get(4)) * mult);
				break;
			case 1:
				res.w((get(6) - get(9)) * mult);
				res.x(biggestVal);
				res.y((get(1) + get(4)) * mult);
				res.z((get(8) + get(2)) * mult);
				break;
			case 2:
				res.w((get(8) - get(2)) * mult);
				res.x((get(1) + get(4)) * mult);
				res.y(biggestVal);
				res.z((get(6) + get(9)) * mult);
				break;
			case 3:
				res.w((get(1) - get(4)) * mult);
				res.x((get(8) + get(2)) * mult);
				res.y((get(6) + get(9)) * mult);
				res.z(biggestVal);
				break;
		}
		
		return res;
	}
	
	private final static FloatBuffer direct = BufferUtils.createFloatBuffer(16);
	
	public FloatBuffer toBuffer() {
		direct.clear();
		direct.put(matrix);
		direct.flip();
		return direct;
	}
}
 public class MatrixStack {
	private Stack<Matrix4> stack;
	private Matrix4 current;
	
	public MatrixStack() {
		current = new Matrix4().clearToIdentity();
		stack = new Stack<>();
	}
	
	public Matrix4 getTop() {
		return current;
	}
	
	public void setTop(Matrix4 m) {
		current = m;
	}
	
	public void pushMatrix() {
		stack.push(current);
		current = new Matrix4(current);
	}
	
	public void popMatrix() {
		current = stack.pop();
	}
}
 public class Quaternion {
	private float x, y, z, w;
	
	public Quaternion() {
		reset();
	}
	
	public Quaternion(float x, float y, float z, float w) {
		set(x, y, z, w);
	}
	
	public Quaternion(float angle, Vector3 vec) {
		float s = (float)Math.sin(angle / 2);
		
		x = vec.x() * s;
		y = vec.y() * s;
		z = vec.z() * s;
		w = (float)Math.cos(angle / 2);
	}
	
	public Quaternion(Quaternion q) {
		set(q);
	}
	
	public Quaternion copy() {
		return new Quaternion(this);
	}
	
	public float x() {
		return x;
	}
	
	public Quaternion x(float x) {
		this.x = x;
		return this;
	}
	
	public float y() {
		return y;
	}
	
	public Quaternion y(float y) {
		this.y = y;
		return this;
	}
	
	public float z() {
		return z;
	}
	
	public Quaternion z(float z) {
		this.z = z;
		return this;
	}
	
	public float w() {
		return w;
	}
	
	public Quaternion w(float w) {
		this.w = w;
		return this;
	}
	
	public Quaternion set(float x, float y, float z, float w) {
		this.x = x;
		this.y = y;
		this.z = z;
		this.w = w;
		return this;
	}
	
	public Quaternion set(Quaternion q) {
		return set(q.x, q.y, q.z, q.w);
	}
	
	public Quaternion reset() {
		x = y = z = 0;
		w = 1;
		return this;
	}
	
	public float length() {
		return (float)Math.sqrt(x * x + y * y + z * z + w * w);
	}
	
	public Quaternion normalize() {
		float length = length();
		x /= length;
		y /= length;
		z /= length;
		w /= length;
		return this;
	}
	
	public float dot(Quaternion q) {
		return x * q.x + y * q.y + z * q.z + w * q.w;
	}
	
	public Quaternion add(float x, float y, float z, float w) {
		this.x += x;
		this.y += y;
		this.z += z;
		this.w += w;
		
		return this;
	}
	
	public Quaternion add(Quaternion q) {
		return add(q.x, q.y, q.z, q.w);
	}
	
	public Quaternion sub(float x, float y, float z, float w) {
		this.x -= x;
		this.y -= y;
		this.z -= z;
		this.w -= w;
		
		return this;
	}
	
	public Quaternion sub(Quaternion q) {
		return sub(q.x, q.y, q.z, q.w);
	}
	
	public Quaternion mult(float f) {
		this.x *= f;
		this.y *= f;
		this.z *= f;
		this.w *= f;
		
		return this;
	}
	
	public Vector3 mult(Vector3 v) {
		Vector3 uv, uuv;
		Vector3 quatVector = new Vector3(x, y, z);
		
		uv = quatVector.cross(v);
		uuv = quatVector.cross(uv);
		
		uv.mult(w * 2);
		uuv.mult(2);
		
		return new Vector3(v).add(uv).add(uuv);
	}
	
	public Quaternion mult(Quaternion q) {
		float xx = w * q.x + x * q.w + y * q.z - z * q.y;
		float yy = w * q.y + y * q.w + z * q.x - x * q.z;
		float zz = w * q.z + z * q.w + x * q.y - y * q.x;
		float ww = w * q.w - x * q.x - y * q.y - z * q.z;
		
		x = xx;
		y = yy;
		z = zz;
		w = ww;
		
		return this;
	}
	
	public Quaternion conjugate() {
		x *= -1;
		y *= -1;
		z *= -1;
		
		return this;
	}
	
	public Matrix4 toMatrix() {
		float[] m = {
				1 - 2 * y * y - 2 * z * z, 2 * x * y + 2 * w * z, 2 * x * z - 2 * w * y, 0,
				2 * x * y - 2 * w * z, 1 - 2 * x * x - 2 * z * z, 2 * y * z + 2 * w * x, 0,
				2 * x * z + 2 * w * y, 2 * y * z - 2 * w * x, 1 - 2 * x * x - 2 * y * y, 0,
				0, 0, 0, 1,
		};
		
		return new Matrix4(m);
	}
}
 public class Vector3 implements Vector<Vector3> {
	private float x, y, z;
	
	public Vector3() {
		set(0, 0, 0);
	}
	
	public Vector3(float v) {
		this(v, v, v);
	}
	
	public Vector3(float x, float y, float z) {
		set(x, y, z);
	}
	
	public Vector3(Vector2 vec) {
		set(vec);
	}
	
	public Vector3(Vector2 vec, float z) {
		set(vec, z);
	}
	
	public Vector3(Vector3 vec) {
		set(vec);
	}
	
	public Vector3(Vector4 vec) {
		set(vec);
	}
	
	@Override
	public Vector3 copy() {
		return new Vector3(this);
	}
	
	public float x() {
		return x;
	}
	
	public Vector3 x(float x) {
		this.x = x;
		return this;
	}
	
	public float y() {
		return y;
	}
	
	public Vector3 y(float y) {
		this.y = y;
		return this;
	}
	
	public float z() {
		return z;
	}
	
	public Vector3 z(float z) {
		this.z = z;
		return this;
	}
	
	@Override
	public boolean equals(Object o) {
		if(o instanceof Vector3) {
			Vector3 v = (Vector3)o;
			return x == v.x && y == v.y && z == v.z;
		}
		
		return false;
	}
	
	public Vector3 set(float x, float y, float z) {
		this.x = x;
		this.y = y;
		this.z = z;
		return this;
	}
	
	public Vector3 set(Vector2 vec) {
		return set(vec.x(), vec.y(), 0);
	}
	
	public Vector3 set(Vector2 vec, float z) {
		return set(vec.x(), vec.y(), z);
	}
	
	public Vector3 set(Vector3 vec) {
		return set(vec.x, vec.y, vec.z);
	}
	
	public Vector3 set(Vector4 vec) {
		return set(vec.x(), vec.y(), vec.z());
	}
	
	public Vector3 reset() {
		x = y = z = 0;
		return this;
	}
	
	@Override
	public float length() {
		return (float)Math.sqrt(x * x + y * y + z * z);
	}
	
	public Vector3 normalize() {
		float length = length();
		x /= length;
		y /= length;
		z /= length;
		return this;
	}
	
	public float dot(Vector3 vec) {
		return x * vec.x + y * vec.y + z * vec.z;
	}
	
	public Vector3 cross(Vector3 vec) {
		return new Vector3(y * vec.z - vec.y * z, z * vec.x - vec.z * x, x * vec.y - vec.x * y);
	}
	
	public Vector3 add(float x, float y, float z) {
		this.x += x;
		this.y += y;
		this.z += z;
		return this;
	}
	
	@Override
	public Vector3 add(Vector3 vec) {
		return add(vec.x, vec.y, vec.z);
	}
	
	public Vector3 sub(float x, float y, float z) {
		this.x -= x;
		this.y -= y;
		this.z -= z;
		return this;
	}
	
	@Override
	public Vector3 sub(Vector3 vec) {
		return sub(vec.x, vec.y, vec.z);
	}
	
	@Override
	public Vector3 mult(float f) {
		return mult(f, f, f);
	}
	
	public Vector3 mult(float x, float y, float z) {
		this.x *= x;
		this.y *= y;
		this.z *= z;
		return this;
	}
	
	@Override
	public Vector3 mult(Vector3 vec) {
		return mult(vec.x, vec.y, vec.z);
	}
	
	@Override
	public Vector3 divide(float f) {
		return divide(f, f, f);
	}
	
	public Vector3 divide(float x, float y, float z) {
		this.x /= x;
		this.y /= y;
		this.z /= z;
		return this;
	}
	
	@Override
	public Vector3 divide(Vector3 vec) {
		return divide(vec.x, vec.y, vec.z);
	}
	
	private final static FloatBuffer direct = BufferUtils.createFloatBuffer(3);
	
	@Override
	public FloatBuffer toBuffer() {
		direct.clear();
		direct.put(x).put(y).put(z);
		direct.flip();
		return direct;
	}
}
 public class Vector4 implements Vector<Vector4> {
	private float x, y, z, w;
	
	public Vector4() {
		set(0, 0, 0, 0);
	}
	
	public Vector4(float v) {
		this(v, v, v, v);
	}
	
	public Vector4(float x, float y, float z, float w) {
		set(x, y, z, w);
	}
	
	public Vector4(Vector2 vec) {
		set(vec);
	}
	
	public Vector4(Vector2 vec, float z, float w) {
		set(vec, z, w);
	}
	
	public Vector4(Vector3 vec) {
		set(vec);
	}
	
	public Vector4(Vector3 vec, float w) {
		set(vec, w);
	}
	
	public Vector4(Vector4 vec) {
		set(vec);
	}
	
	@Override
	public Vector4 copy() {
		return new Vector4(this);
	}
	
	public float x() {
		return x;
	}
	
	public Vector4 x(float x) {
		this.x = x;
		return this;
	}
	
	public float y() {
		return y;
	}
	
	public Vector4 y(float y) {
		this.y = y;
		return this;
	}
	
	public float z() {
		return z;
	}
	
	public Vector4 z(float z) {
		this.z = z;
		return this;
	}
	
	public float w() {
		return w;
	}
	
	public Vector4 w(float w) {
		this.w = w;
		return this;
	}
	
	@Override
	public boolean equals(Object o) {
		if(o instanceof Vector4) {
			Vector4 v = (Vector4)o;
			return x == v.x && y == v.y && z == v.z && w == v.w;
		}
		
		return false;
	}
	
	public Vector4 set(float x, float y, float z, float w) {
		this.x = x;
		this.y = y;
		this.z = z;
		this.w = w;
		return this;
	}
	
	public Vector4 set(Vector2 vec) {
		return set(vec, 0, 0);
	}
	
	public Vector4 set(Vector2 vec, float z, float w) {
		return set(vec.x(), vec.y(), z, w);
	}
	
	public Vector4 set(Vector3 vec) {
		return set(vec, 0);
	}
	
	public Vector4 set(Vector3 vec, float w) {
		return set(vec.x(), vec.y(), vec.z(), w);
	}
	
	public Vector4 set(Vector4 vec) {
		return set(vec.x, vec.y, vec.z, vec.w);
	}
	
	public Vector4 reset() {
		x = y = z = w = 0;
		return this;
	}
	
	@Override
	public float length() {
		return (float)Math.sqrt(x * x + y * y + z * z + w * w);
	}
	
	public Vector4 normalize() {
		float length = length();
		x /= length;
		y /= length;
		z /= length;
		w /= length;
		return this;
	}
	
	public float dot(Vector4 vec) {
		return x * vec.x + y * vec.y + z * vec.z + w * vec.w;
	}
	
	public Vector4 add(float x, float y, float z, float w) {
		this.x += x;
		this.y += y;
		this.z += z;
		this.w += w;
		return this;
	}
	
	@Override
	public Vector4 add(Vector4 vec) {
		return add(vec.x, vec.y, vec.z, vec.w);
	}
	
	public Vector4 sub(float x, float y, float z, float w) {
		this.x -= x;
		this.y -= y;
		this.z -= z;
		this.w -= w;
		return this;
	}
	
	@Override
	public Vector4 sub(Vector4 vec) {
		return sub(vec.x, vec.y, vec.z, vec.w);
	}
	
	@Override
	public Vector4 mult(float f) {
		return mult(f, f, f, f);
	}
	
	public Vector4 mult(float x, float y, float z, float w) {
		this.x *= x;
		this.y *= y;
		this.z *= z;
		this.w *= w;
		return this;
	}
	
	@Override
	public Vector4 mult(Vector4 vec) {
		return mult(vec.x, vec.y, vec.z, vec.w);
	}
	
	@Override
	public Vector4 divide(float f) {
		return divide(f, f, f, f);
	}
	
	public Vector4 divide(float x, float y, float z, float w) {
		this.x /= x;
		this.y /= y;
		this.z /= z;
		this.w /= w;
		return this;
	}
	
	@Override
	public Vector4 divide(Vector4 vec) {
		return divide(vec.x, vec.y, vec.z, vec.w);
	}
	
	private final static FloatBuffer direct = BufferUtils.createFloatBuffer(4);
	
	@Override
	public FloatBuffer toBuffer() {
		direct.clear();
		direct.put(x).put(y).put(z).put(w);
		direct.flip();
		return direct;
	}
}

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL15.*;
import static org.lwjgl.opengl.GL20.*;
import static org.lwjgl.opengl.GL30.*;
import static org.lwjgl.opengl.GL31.*;
import static org.lwjgl.opengl.GL32.*;
import org.lwjgl.input.Keyboard;
import com.ra4king.opengl.GLProgram;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.LightManager;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.LightManager.LightBlock;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.LightManager.LightBlockGamma;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.LightManager.SunlightValueHDR;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.LightManager.TimerTypes;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.Scene;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.Scene.LightingProgramTypes;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.Scene.ProgramData;
import com.ra4king.opengl.arcsynthesis.gl33.chapter12.Scene.ProgramStore;
import com.ra4king.opengl.util.MousePoles.MouseButton;
import com.ra4king.opengl.util.MousePoles.ViewData;
import com.ra4king.opengl.util.MousePoles.ViewPole;
import com.ra4king.opengl.util.MousePoles.ViewScale;
import com.ra4king.opengl.util.ShaderProgram;
import com.ra4king.opengl.util.Timer.Type;
import com.ra4king.opengl.util.Utils;
import com.ra4king.opengl.util.math.Matrix4;
import com.ra4king.opengl.util.math.MatrixStack;
import com.ra4king.opengl.util.math.Quaternion;
import com.ra4king.opengl.util.math.Vector3;
import com.ra4king.opengl.util.math.Vector4;

package com.ra4king.opengl.arcsynthesis.gl33.chapter12.example3;




public class Example12_3 extends GLProgram {
	public static void main(String[] args) {
		new Example12_3().run(true);
	}
	
	private ProgramData[] programs = new ProgramData[LightingProgramTypes.values().length];
	private UnlitProgramData unlit;
	
	private Scene scene;
	private LightManager lights;
	
	private ViewPole viewPole;
	
	private Vector4 skyDaylightColor = new Vector4(0.65f, 0.65f, 1, 1);
	
	private TimerTypes timerMode = TimerTypes.TIMER_ALL;
	
	private final int materialBlockIndex = 0;
	private final int lightBlockIndex = 1;
	private final int projectionBlockIndex = 2;
	
	private int lightUniformBuffer;
	private int projectionUniformBuffer;
	
	private boolean drawCameraPos, drawLights = true;
	
	private boolean isGammaCorrect = false;
	private float gammaValue = 2.2f;
	
	public Example12_3() {
		super("Example 12.3 - Gamma Correction", 800, 800, true);
	}
	
	@Override
	public void init() {

		ViewData viewData = new ViewData(new Vector3(-59.5f, 44, 95), new Quaternion(0.3826834f, 0.0f, 0.0f, 0.92387953f), 50, 0);
