public class DdsLoader {
	public static ImageSet load(InputStream dds) throws IOException {
		byte[] ddsFile = readDdsFile(dds);
		
		// Check the first 4 bytes.
		int magicTest = readDoubleWord(ddsFile, 0);
		if(magicTest != MagicNumbers.DDS_MAGIC_NUMBER) {
			throw new DdsFileMalformedException("The Magic number is missing from the file.");
		}
		
		if(ddsFile.length < DdsHeader.SIZE + 4) {
			throw new DdsFileMalformedException("The data is way too small to store actual information.");
		}
		
		// Collect info from the DDS file.
		DdsHeader ddsHeader = new DdsHeader(ddsFile);
		Dds10Header dds10Header = getDds10Header(ddsFile, ddsHeader);
		Dimensions ddsDimensions = getDimensions(ddsHeader);
		UncheckedImageFormat ddsFormat = getFormat(ddsHeader, dds10Header);
		
		// Get image counts.
		int numArrays = (dds10Header.arraySize > 1) ? dds10Header.arraySize : 1;
		int numFaces = (dds10Header.miscFlag & Dds10MiscFlags.RESOURCE_MISC_TEXTURECUBE) != 0 ? 6 : 1;
		int numMipmaps = (ddsHeader.flags & DdsFlags.MIPMAPCOUNT) != 0 ? ddsHeader.mipmapCount : 1;
		
		int baseOffset = getOffsetToData(ddsHeader);
		
		// Build the image creator.
		ImageCreator imageCreator = new ImageCreator(new ImageFormat(ddsFormat), ddsDimensions, numMipmaps, numArrays, numFaces);
		int cumulativeOffset = baseOffset;
		
		for(int arrayIx = 0; arrayIx < numArrays; arrayIx++) {
			for(int faceIx = 0; faceIx < numFaces; faceIx++) {
				for(int mipmapLevel = 0; mipmapLevel < numMipmaps; mipmapLevel++) {
					int mipmapLevelSize = calcMipmapSize(ddsFormat, ddsDimensions, mipmapLevel);
					
					// Get specific data from ddsFile.
					byte[] mipmapLevelData = Arrays.copyOfRange(ddsFile, cumulativeOffset, cumulativeOffset + mipmapLevelSize);
					
					// Set data for the current mipmap level in imageCreator.
					imageCreator.setImageData(mipmapLevelData, true, mipmapLevel, arrayIx, faceIx);
					
					// Advance offset to read next mipmapLevelData from ddsFile.
					cumulativeOffset += mipmapLevelSize;
				}
			}
		}
		
		return imageCreator.createImage();
	}
	
	private static class DdsFileMalformedException extends RuntimeException {
		private static final long serialVersionUID = -1523221969465221880L;
		
		private DdsFileMalformedException(String message) {
			super(message);
		}
	}
	
	private static class DdsFileUnsupportedException extends RuntimeException {
		private static final long serialVersionUID = 377383320427260974L;
		
		private DdsFileUnsupportedException(String filename, String message) {
			super(filename + ": " + message);
		}
	}
	
	private static class MagicNumbers {
		static final int DDS_MAGIC_NUMBER = 0x20534444; // "DDS "
		static final int DDS10_FOUR_CC = 0x30314458; // "DX10"
		
		static final int DDSFOURCC_DXT1 = 0x31545844; // "DXT1"
		static final int DDSFOURCC_DXT3 = 0x33545844; // "DXT3"
		static final int DDSFOURCC_DXT5 = 0x35545844; // "DXT5"
	}
	
	@SuppressWarnings("unused")
	private static class DdsFlags {
		static final int CAPS = 0x00000001;
		static final int HEIGHT = 0x00000002;
		static final int WIDTH = 0x00000004;
		static final int PITCH = 0x00000008;
		static final int PIXELFORMAT = 0x00001000;
		static final int MIPMAPCOUNT = 0x00020000;
		static final int LINEARSIZE = 0x00080000;
		static final int DEPTH = 0x00800000;
		
	}
	
	private static class Dds10MiscFlags {
		static final int RESOURCE_MISC_TEXTURECUBE = 0x00000004;
	}
	
	@SuppressWarnings("unused")
	private static class Dds10ResourceDimensions {
		static final int TEXTURE1D = 2;
		static final int TEXTURE2D = 3;
		static final int TEXTURE3D = 4;
	}
	
	private static class DdsCaps2 {
		static final int CUBEMAP = 0x00000200;
		static final int CUBEMAP_POSITIVEX = 0x00000400;
		static final int CUBEMAP_NEGATIVEX = 0x00000800;
		static final int CUBEMAP_POSITIVEY = 0x00001000;
		static final int CUBEMAP_NEGATIVEY = 0x00002000;
		static final int CUBEMAP_POSITIVEZ = 0x00004000;
		static final int CUBEMAP_NEGATIVEZ = 0x00008000;
		static final int VOLUME = 0x00200000;
		
		static final int CUBEMAP_ALL = CUBEMAP |
				CUBEMAP_POSITIVEX | CUBEMAP_NEGATIVEX |
				CUBEMAP_POSITIVEY | CUBEMAP_NEGATIVEY |
				CUBEMAP_POSITIVEZ | CUBEMAP_NEGATIVEZ;
	}
	
	private static class DxgiFormat {
		static final int UNKNOWN = 0;
	}
	
	@SuppressWarnings("unused")
	private static class DdsPixelFormat {
		int size;
		int flags;
		int fourCC;
		int rgbBitCount;
		int rBitMask;
		int gBitMask;
		int bBitMask;
		int aBitMask;
	}
	
	@SuppressWarnings("unused")
	private static class DdsPixelFormatFlags {
		static final int ALPHAPIXELS = 0x00000001;
		static final int ALPHA = 0x00000002;
		static final int FOURCC = 0x00000004;
		static final int RGB = 0x00000040;
		static final int YUV = 0x00000200;
		static final int LUMINANCE = 0x00020000;
	}
	
	@SuppressWarnings("unused")
	private static class DdsHeader {
		static final int SIZE = (7 + 11 + 8 + 5) * (Integer.SIZE / 8);
		
		int size;
		int flags;
		int height;
		int width;
		int pitchOrLinearSize;
		int depth;
		int mipmapCount;
		
		int reserved1[] = new int[11];
		
		DdsPixelFormat ddsPixelFormat;
		
		int caps;
		int caps2;
		int caps3;
		int caps4;
		int reserved2;
		
		DdsHeader(byte[] ddsFile) {
			size = readDoubleWord(ddsFile, 4);
			flags = readDoubleWord(ddsFile, 8);
			height = readDoubleWord(ddsFile, 12);
			width = readDoubleWord(ddsFile, 16);
			pitchOrLinearSize = readDoubleWord(ddsFile, 20);
			depth = readDoubleWord(ddsFile, 24);
			mipmapCount = readDoubleWord(ddsFile, 28);
			
			for(int i = 0; i < reserved1.length; i++) {
				reserved1[i] = readDoubleWord(ddsFile, 32 + 4 * i);
			}
			
			ddsPixelFormat = new DdsPixelFormat();
			ddsPixelFormat.size = readDoubleWord(ddsFile, 76);
			ddsPixelFormat.flags = readDoubleWord(ddsFile, 80);
			ddsPixelFormat.fourCC = readDoubleWord(ddsFile, 84);
			ddsPixelFormat.rgbBitCount = readDoubleWord(ddsFile, 88);
			ddsPixelFormat.rBitMask = readDoubleWord(ddsFile, 92);
			ddsPixelFormat.gBitMask = readDoubleWord(ddsFile, 96);
			ddsPixelFormat.bBitMask = readDoubleWord(ddsFile, 100);
			ddsPixelFormat.aBitMask = readDoubleWord(ddsFile, 104);
			
			caps = readDoubleWord(ddsFile, 108);
			caps2 = readDoubleWord(ddsFile, 112);
			caps3 = readDoubleWord(ddsFile, 116);
			caps4 = readDoubleWord(ddsFile, 120);
			reserved2 = readDoubleWord(ddsFile, 124);
		}
	}
	
	@SuppressWarnings("unused")
	private static class Dds10Header {
		static final int SIZE = 5 * (Integer.SIZE / 8);
		
		int dxgiFormat;
		int resourceDimension;
		int miscFlag;
		int arraySize;
		int reserved;
	}
	
	private static byte[] readDdsFile(InputStream ddsInStream) throws IOException {
		assert ddsInStream != null;
		
		ByteArrayOutputStream ddsOutStream = new ByteArrayOutputStream();
		int bytesRead;
		
		byte[] buffer = new byte[4096];
		
		while((bytesRead = ddsInStream.read(buffer)) != -1) {
			ddsOutStream.write(buffer, 0, bytesRead);
		}
		
		return ddsOutStream.toByteArray();
	}
	
	// Read four bytes.
	private static int readDoubleWord(byte bytes[], int startIx) {
		int dw = 0;
		
		// Read 4 bytes.
		for(int i = startIx; i < startIx + 4; i++) {
			dw += (bytes[i] & 0xff) << (8 * (i - startIx));
		}
		
		return dw;
	}
	
	// Will either generate this or return the actual one.
	private static Dds10Header getDds10Header(byte[] ddsFile, DdsHeader ddsHeader) {
		if(ddsHeader.ddsPixelFormat.fourCC == MagicNumbers.DDS10_FOUR_CC) {
			Dds10Header dds10Header = new Dds10Header();
			int offsetToNewHeader = DdsHeader.SIZE + 4;
			
			dds10Header.dxgiFormat = readDoubleWord(ddsFile, offsetToNewHeader);
			dds10Header.resourceDimension = readDoubleWord(ddsFile, offsetToNewHeader + 4);
			dds10Header.miscFlag = readDoubleWord(ddsFile, offsetToNewHeader + 8);
			dds10Header.arraySize = readDoubleWord(ddsFile, offsetToNewHeader + 12);
			dds10Header.reserved = readDoubleWord(ddsFile, offsetToNewHeader + 16);
			
			return dds10Header;
		}
		
		// Compute the header manually. Namely, compute the DXGI_FORMAT for the given data.
		Dds10Header dds10header = new Dds10Header();
		
		// Get dimensionality. Assume 2D unless otherwise stated.
		dds10header.resourceDimension = Dds10ResourceDimensions.TEXTURE2D;
		
		if((ddsHeader.caps2 & DdsCaps2.VOLUME) != 0 && (ddsHeader.flags & DdsFlags.DEPTH) != 0) {
			dds10header.resourceDimension = Dds10ResourceDimensions.TEXTURE3D;
		}
		
		// Get cubemap.
		int cubemapTest = ddsHeader.caps2 & DdsCaps2.CUBEMAP_ALL;
		if(cubemapTest == 0) {
			dds10header.miscFlag = 0;
		} else {
			// All faces must be specified or none. Otherwise unsupported.
			if(cubemapTest != DdsCaps2.CUBEMAP_ALL) {
				throw new DdsFileUnsupportedException("", "All cubemap faces must be specified.");
			}
			
			dds10header.miscFlag = Dds10MiscFlags.RESOURCE_MISC_TEXTURECUBE;
		}
		
		// Array size is... zero?
		dds10header.arraySize = 0;
		
		// Use the old-style format.
		dds10header.dxgiFormat = DxgiFormat.UNKNOWN;
		
		return dds10header;
	}
	
	private static Dimensions getDimensions(DdsHeader ddsHeader) {
		Dimensions ddsDimensions = new Dimensions();
		ddsDimensions.numDimensions = 1;
		ddsDimensions.width = ddsHeader.width;
		
		if((ddsHeader.flags & DdsFlags.HEIGHT) != 0) {
			ddsDimensions.numDimensions = 2;
			ddsDimensions.height = ddsHeader.height;
		}
		
		if((ddsHeader.flags & DdsFlags.DEPTH) != 0) {
			ddsDimensions.numDimensions = 3;
			ddsDimensions.depth = ddsHeader.depth;
		}
		
		return ddsDimensions;
	}
	
	private static UncheckedImageFormat getFormat(DdsHeader ddsHeader, Dds10Header dds10Header) throws DdsFileUnsupportedException {
		if(dds10Header.dxgiFormat != DxgiFormat.UNKNOWN) {
			throw new IllegalArgumentException("Format is not unknown.");
		}
		
		for(OldDdsFormatConv o : s_oldFormatConvert)
			if(doesMatchFormat(o.ddsFmt, ddsHeader))
				return o.uncheckedImageFormat;
		
		throw new DdsFileUnsupportedException("", "Could not use the DDS9's image format.");
	}
	
	private static int getOffsetToData(DdsHeader ddsHeader) {
		int byteOffset = DdsHeader.SIZE + 4;
		
		if(ddsHeader.ddsPixelFormat.fourCC == MagicNumbers.DDS10_FOUR_CC) {
			byteOffset += Dds10Header.SIZE;
		}
		
		return byteOffset;
	}
	
	// Computes the bytesize of a single scanline of an image of the given format, with the given line width.
	// For compressed textures, the value returned is the number of bytes for every 4 scanlines.
	private static int calcLineSize(UncheckedImageFormat ddsFormat, int lineWidth) {
		// This is from the DDS suggestions for line size computations.
		if(ddsFormat.bitDepth == BitDepth.COMPRESSED) {
			int blockSize = 16;
			
			if(ddsFormat.type == PixelDataType.COMPRESSED_BC1
					|| ddsFormat.type == PixelDataType.COMPRESSED_UNSIGNED_BC4
					|| ddsFormat.type == PixelDataType.COMPRESSED_SIGNED_BC4) {
				blockSize = 8;
			}
			
			return ((lineWidth + 3) / 4) * blockSize;
		} else {
			int bytesPerPixel = DdsUtil.calcBytesPerPixel(new ImageFormat(ddsFormat));
			
			return lineWidth * bytesPerPixel;
		}
	}
	
	private static int calcMipmapSize(UncheckedImageFormat ddsFormat, Dimensions ddsDimensions, int mipmapLevel) {
		Dimensions mipmapDimensions = DdsUtil.calcMipmapLevelDimensions(ddsDimensions, mipmapLevel);
		int lineSize = calcLineSize(ddsFormat, mipmapDimensions.width);
		
		int effectiveHeight = 1;
		if(mipmapDimensions.numDimensions > 1) {
			effectiveHeight = mipmapDimensions.height;
			
			if(ddsFormat.bitDepth == BitDepth.COMPRESSED) {
				effectiveHeight = (effectiveHeight + 3) / 4;
			}
		}
		
		int effectiveDepth = 1;
		if(mipmapDimensions.numDimensions > 2) {
			effectiveDepth = mipmapDimensions.depth;
			
			if(ddsFormat.bitDepth == BitDepth.COMPRESSED) {
				effectiveDepth = (effectiveDepth + 3) / 4;
			}
		}
		
		int numLines = effectiveHeight * effectiveDepth;
		
		return lineSize * numLines;
	}
	
	private static OldDdsFormatConv s_oldFormatConvert[] = {
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGBA, ComponentOrder.RGBA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB | DdsPixelFormatFlags.ALPHAPIXELS,
					32, 0xff, 0xff00, 0xff0000, 0xff000000, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGBA, ComponentOrder.BGRA,
					BitDepth.PACKED_32_BIT_8888_REV, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB | DdsPixelFormatFlags.ALPHAPIXELS,
					32, 0xff0000, 0xff00, 0xff, 0xff000000, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGBX, ComponentOrder.RGBA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 32, 0xff, 0xff00, 0xff0000, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGB, ComponentOrder.RGBA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 24, 0xff, 0xff00, 0xff0000, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGB, ComponentOrder.RGBA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 24, 0xff, 0xff00, 0xff0000, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGB, ComponentOrder.BGRA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 24, 0xff0000, 0xff00, 0xff, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGB, ComponentOrder.RGBA,
					BitDepth.PACKED_16_BIT_565, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 16, 0xf800, 0x7e0, 0x1f, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGBA, ComponentOrder.BGRA,
					BitDepth.PACKED_16_BIT_1555_REV, 1),
					new OldDdsFmtMatch(DdsPixelFormatFlags.RGB, 16, 0x7c00,
							0x3e0, 0x1f, 0x8000, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RGBA, ComponentOrder.BGRA,
					BitDepth.PACKED_16_BIT_4444_REV, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 16, 0xf00, 0xf0, 0xf, 0xf000, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.COMPRESSED_BC1, PixelComponents.COLOR_RGB,
					ComponentOrder.COMPRESSED, BitDepth.COMPRESSED, 1),
					new OldDdsFmtMatch(DdsPixelFormatFlags.FOURCC, 0, 0, 0, 0,
							0, MagicNumbers.DDSFOURCC_DXT1)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.COMPRESSED_BC2, PixelComponents.COLOR_RGBA,
					ComponentOrder.COMPRESSED, BitDepth.COMPRESSED, 1),
					new OldDdsFmtMatch(DdsPixelFormatFlags.FOURCC, 0, 0, 0, 0,
							0, MagicNumbers.DDSFOURCC_DXT3)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.COMPRESSED_BC3, PixelComponents.COLOR_RGBA,
					ComponentOrder.COMPRESSED, BitDepth.COMPRESSED, 1),
					new OldDdsFmtMatch(DdsPixelFormatFlags.FOURCC, 0, 0, 0, 0,
							0, MagicNumbers.DDSFOURCC_DXT5)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RG, ComponentOrder.RGBA,
					BitDepth.PER_COMP_16, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 32, 0xffff, 0xffff0000, 0, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RG, ComponentOrder.RGBA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.RGB, 16, 0xffff, 0xffff0000, 0, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RED, ComponentOrder.RGBA,
					BitDepth.PER_COMP_16, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.LUMINANCE, 16, 0xffff, 0, 0, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RED, ComponentOrder.RGBA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.LUMINANCE, 8, 0xff, 0, 0, 0, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RG, ComponentOrder.RGBA,
					BitDepth.PER_COMP_16, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.LUMINANCE
							| DdsPixelFormatFlags.ALPHAPIXELS, 16, 0xffff, 0,
					0, 0xffff0000, 0)),
			
			new OldDdsFormatConv(new UncheckedImageFormat(
					PixelDataType.NORM_UNSIGNED_INTEGER,
					PixelComponents.COLOR_RG, ComponentOrder.RGBA,
					BitDepth.PER_COMP_8, 1), new OldDdsFmtMatch(
					DdsPixelFormatFlags.LUMINANCE
							| DdsPixelFormatFlags.ALPHAPIXELS, 8, 0xff, 0, 0,
					0xff00, 0)), };
	
	private static class OldDdsFmtMatch {
		int flags;
		int bitDepth;
		int rBitmask;
		int gBitmask;
		int bBitmask;
		int aBitmask;
		int fourCC;
		
		OldDdsFmtMatch(int flags, int bitDepth, int rBitmask, int gBitmask,
				int bBitmask, int aBitmask, int fourCC) {
			this.flags = flags;
			this.bitDepth = bitDepth;
			this.rBitmask = rBitmask;
			this.gBitmask = gBitmask;
			this.bBitmask = bBitmask;
			this.aBitmask = aBitmask;
			this.fourCC = fourCC;
		}
	}
	
	private static class OldDdsFormatConv {
		UncheckedImageFormat uncheckedImageFormat;
		OldDdsFmtMatch ddsFmt;
		
		OldDdsFormatConv(UncheckedImageFormat uncheckedImageFormat, OldDdsFmtMatch ddsFmt) {
			this.uncheckedImageFormat = uncheckedImageFormat;
			this.ddsFmt = ddsFmt;
		}
	}
	
	private static boolean doesMatchFormat(OldDdsFmtMatch ddsFmt, DdsHeader ddsHeader) {
		if((ddsHeader.ddsPixelFormat.flags & ddsFmt.flags) == 0) {
			return false;
		}
		
		if((ddsFmt.flags & DdsPixelFormatFlags.FOURCC) != 0) {
			// None of the bit counts matter. Just check the fourCC
			if(ddsFmt.fourCC != ddsHeader.ddsPixelFormat.fourCC) {
				return false;
			}
		} else {
			// Check the bitcounts, not the fourCC.
			if(ddsHeader.ddsPixelFormat.rgbBitCount != ddsFmt.bitDepth) {
				return false;
			}
			
			if((ddsFmt.rBitmask & ddsHeader.ddsPixelFormat.rBitMask) != ddsFmt.rBitmask) {
				return false;
			}
			
			if((ddsFmt.gBitmask & ddsHeader.ddsPixelFormat.gBitMask) != ddsFmt.gBitmask) {
				return false;
			}
			
			if((ddsFmt.bBitmask & ddsHeader.ddsPixelFormat.bBitMask) != ddsFmt.bBitmask) {
				return false;
			}
			
			if((ddsFmt.aBitmask & ddsHeader.ddsPixelFormat.aBitMask) != ddsFmt.aBitmask) {
				return false;
			}
		}
		
		return true;
	}
}
 public class TextureGenerator {
	
	public static class ForcedConvertFlags {
		public static int FORCE_SRGB_COLORSPACE_FMT = 0x0001; // When possible, will force the format to use the sRGB colorspace. Does not cause erroring for formats that can't be sRGB, unless your GL
																// implementation doesn't support sRGB.
		public static int FORCE_BC1_ALPHA_FMT = 0x0002; // When used with a BC1 texture, will force the texture to have an alpha. Ignored otherwise.
		public static int FORCE_LUMINANCE_FMT = 0x0008; // Red and RG textures will become luminance and luminance/alpha textures in all cases. Exceptions will be thrown if the GL implementation does
														// not support those luminance/alpha formats (ie: is core).
		
		public static int FORCE_INTEGRAL_FMT = 0x0020; // Image formats that contain normalized integers will be uploaded as non-normalized integers. Ignored for floating-point or compressed formats.
		public static int FORCE_SIGNED_FMT = 0x0040; // Image formats that contain unsigned integers will be uploaded as signed integers. Ignored if the format is not an integer/integral format, or if
														// it isn't BC4 or BC5 compressed.
		public static int FORCE_COLOR_RENDERABLE_FMT = 0x0080; // NOT YET SUPPORTED! Will force the use of formats that are required to be valid render targets. This will add components if necessary,
																// but it will throw if conversion would require fundamentally changing the basic format (from signed to unsigned, compressed textures,
																// etc).
		
		public static int FORCE_ARRAY_TEXTURE = 0x0004; // NOT YET SUPPORTED! The texture will be an array texture even if the depth is not present. Ignored for formats that can't be arrays. Will
														// throw if array textures of that type are not supported (ie: cubemap arrays, 2D arrays for lesser hardware, etc).
		public static int USE_TEXTURE_STORAGE = 0x0100; // If ARB_texture_storage or GL 4.2 is available, then texture storage functions will be used to create the textures. Otherwise regular glTex*
														// functions will be used.
		public static int FORCE_TEXTURE_STORAGE = 0x0200; // If ARB_texture_storage or GL 4.2 is available, then texture storage functions will be used to create the textures. Otherwise, an exception
															// will be thrown.
		public static int USE_DSA = 0x0400; // If EXT_direct_state_access is available, then DSA functions will be used to create the texture. Otherwise, regular ones will be used.
		public static int FORCE_DSA = 0x0800; // If EXT_direct_state_access is available, then DSA functions will be used to create the texture. Otherwise, an exception will be thrown.
	}
	
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */
	
	public static int createTexture(ImageSet imageSet, int forceConvertBits) {
		int textureName = glGenTextures();
		
		try {
			createTexture(textureName, imageSet, forceConvertBits);
		} catch(Exception e) {
			e.printStackTrace();
			glDeleteTextures(textureName);
		}
		
		return textureName;
	}
	
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */
	
	public static class OpenGLPixelTransferParams {
		public int format; // The GLenum format value of pixel transfer operations.
		public int type; // The GLenum type value of pixel transfer operations.
		public int blockByteCount; // The size in bytes for blocks in compressed formats. Necessary to compute the size of the pixel data.
	}
	
	public static int getInternalFormat(ImageFormat imageFormat, int forceConvertBits) {
		int internalFormat = getStandardOpenGLFormat(imageFormat, forceConvertBits);
		
		boolean bConvertToLA = useLAInsteadOfRG(forceConvertBits);
		
		// Convert any R or RG formats to L or LA formats.
		switch(internalFormat) {
			case GL_COMPRESSED_RED_RGTC1:
				return LARG_COMPRESSED_CONV(bConvertToLA, internalFormat, GL_COMPRESSED_LUMINANCE_LATC1_EXT);
			case GL_COMPRESSED_SIGNED_RED_RGTC1:
				return LARG_COMPRESSED_CONV(bConvertToLA, internalFormat, GL_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT);
				// case GL_COMPRESSED_RG_RGTC2:
				// return LARG_COMPRESSED_CONV(bConvertToLA, internalFormat, GL_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT);
				// case GL_COMPRESSED_SIGNED_RG_RGTC2:
				// return LARG_COMPRESSED_CONV(bConvertToLA, internalFormat, GL_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT);
				
			case GL_R8I:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE8I_EXT);
			case GL_R16I:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE16I_EXT);
			case GL_R32I:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE32I_EXT);
			case GL_RG8I:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA8I_EXT);
			case GL_RG16I:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA16I_EXT);
			case GL_RG32I:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA32I_EXT);
				
			case GL_R8UI:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE8UI_EXT);
			case GL_R16UI:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE16UI_EXT);
			case GL_R32UI:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE32UI_EXT);
			case GL_RG8UI:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA8UI_EXT);
			case GL_RG16UI:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA16UI_EXT);
			case GL_RG32UI:
				return LARG_INTEGRAL_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA32UI_EXT);
				
			case GL_R16F:
				return LARG_FLOAT_CONV(bConvertToLA, internalFormat, GL_LUMINANCE16F_ARB);
			case GL_R32F:
				return LARG_FLOAT_CONV(bConvertToLA, internalFormat, GL_LUMINANCE32F_ARB);
			case GL_RG16F:
				return LARG_FLOAT_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA16F_ARB);
			case GL_RG32F:
				return LARG_FLOAT_CONV(bConvertToLA, internalFormat, GL_LUMINANCE_ALPHA32F_ARB);
				
			case EXTTextureSnorm.GL_R8_SNORM:
				return LARG_SNORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE8_SNORM);
			case EXTTextureSnorm.GL_R16_SNORM:
				return LARG_SNORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE16_SNORM);
			case EXTTextureSnorm.GL_RG8_SNORM:
				return LARG_SNORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE8_ALPHA8_SNORM);
			case EXTTextureSnorm.GL_RG16_SNORM:
				return LARG_SNORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE16_ALPHA16_SNORM);
				
			case GL_R8:
				return LARG_NORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE8);
			case GL_R16:
				return LARG_NORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE16);
			case GL_RG8:
				return LARG_NORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE8_ALPHA8);
			case GL_RG16:
				return LARG_NORM_CONV(bConvertToLA, internalFormat, GL_LUMINANCE16_ALPHA16);
		}
		
		return internalFormat;
	}
	
	public static OpenGLPixelTransferParams getUploadFormatType(ImageFormat imageFormat, int forceConvertBits) {
		OpenGLPixelTransferParams upload = new OpenGLPixelTransferParams();
		upload.type = 0xFFFFFFFF;
		upload.format = 0xFFFFFFFF;
		upload.blockByteCount = 0;
		
		PixelDataType pixelDataType = getDataType(imageFormat, forceConvertBits);
		if(pixelDataType.ordinal() >= PixelDataType.NUM_UNCOMPRESSED_TYPES.ordinal()) {
			switch(pixelDataType) {
				case COMPRESSED_BC1:
				case COMPRESSED_UNSIGNED_BC4:
				case COMPRESSED_SIGNED_BC4:
					upload.blockByteCount = 8;
					break;
				
				default:
					upload.blockByteCount = 16;
					break;
			}
			
			// Provide reasonable parameters.
			upload.format = GL_RGBA;
			
			// if(pixelDataType != PixelDataType.COMPRESSED_UNSIGNED_BC6H
			// || pixelDataType != PixelDataType.COMPRESSED_SIGNED_BC6H) {
			upload.type = GL_FLOAT;
			// } else {
			// upload.type = GL_UNSIGNED_BYTE;
			// }
			//
			return upload;
		}
		
		upload.type = getOpenGLType(imageFormat, pixelDataType);
		upload.format = getOpenGLFormat(imageFormat, pixelDataType, forceConvertBits);
		
		return upload;
	}
	
	public static int getTextureType(ImageSet imageSet, int forceConvertBits) {
		Dimensions dims = imageSet.getDimensions();
		
		switch(dims.numDimensions) {
			case 1:
				// May be 1D or 1D array.
				if(isArrayTexture(imageSet, forceConvertBits)) {
					return GL_TEXTURE_1D_ARRAY;
				} else {
					return GL_TEXTURE_1D;
				}
				
			case 2:
				// 2D, 2D array, 2D cube, or 2D array cube.
				if(isArrayTexture(imageSet, forceConvertBits)) {
					if(imageSet.getFaceCount() > 1) {
						return GL_TEXTURE_CUBE_MAP_ARRAY;
					} else {
						return GL_TEXTURE_2D_ARRAY;
					}
				} else {
					if(imageSet.getFaceCount() > 1) {
						return GL_TEXTURE_CUBE_MAP;
					} else {
						return GL_TEXTURE_2D;
					}
				}
				
			case 3:
				// 3D.
				return GL_TEXTURE_3D;
		}
		
		return -1;
	}
	
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */
	
	private static int[] s_packedTypes = {
			GL_UNSIGNED_SHORT_5_6_5, // BitDepth.PACKED_16_BIT_565
			GL_UNSIGNED_SHORT_5_5_5_1, // BitDepth.PACKED_16_BIT_5551
			GL_UNSIGNED_SHORT_4_4_4_4, // BitDepth.PACKED_16_BIT_4444
			GL_UNSIGNED_INT_8_8_8_8, // BitDepth.PACKED_32_BIT_8888
			GL_UNSIGNED_INT_10_10_10_2, // BitDepth.PACKED_32_BIT_1010102
			GL_UNSIGNED_INT_24_8, // BitDepth.PACKED_32_BIT_248
			
			GL_UNSIGNED_SHORT_5_6_5_REV, // BitDepth.PACKED_16_BIT_565_REV
			GL_UNSIGNED_SHORT_1_5_5_5_REV, // BitDepth.PACKED_16_BIT_1555_REV
			GL_UNSIGNED_SHORT_4_4_4_4_REV, // BitDepth.PACKED_16_BIT_4444_REV
			
			GL_UNSIGNED_INT_8_8_8_8_REV, // BitDepth.PACKED_32_BIT_8888_REV
			GL_UNSIGNED_INT_2_10_10_10_REV, // BitDepth.PACKED_32_BIT_2101010_REV
			GL_UNSIGNED_INT_10F_11F_11F_REV, // BitDepth.PACKED_32_BIT_101111_REV
			GL_UNSIGNED_INT_5_9_9_9_REV, // BitDepth.PACKED_32_BIT_5999_REV
	};
	
	// Starts from PixelComponents.COLOR_RGB. non-integral vs. integral.
	private static int[] s_bgraFormats = {
			GL_BGR, GL_BGR_INTEGER, // PixelComponents.COLOR_RGB
			GL_BGRA, GL_BGRA_INTEGER, // PixelComponents.COLOR_RGBX
			GL_BGRA, GL_BGRA_INTEGER, // PixelComponents.COLOR_RGBA
			GL_BGR, GL_BGR_INTEGER, // PixelComponents.COLOR_RGB_sRGB
			GL_BGRA, GL_BGRA_INTEGER, // PixelComponents.COLOR_RGBX_sRGB
			GL_BGRA, GL_BGRA_INTEGER, // PixelComponents.COLOR_RGBA_sRGB
	};
	
	// Non-integral vs. integral.
	private static int[] s_rgbaFormats = {
			GL_RED, GL_RED_INTEGER, // PixelComponents.COLOR_RED
			GL_RG, GL_RG_INTEGER, // PixelComponents.COLOR_RG
			GL_RGB, GL_RGB_INTEGER, // PixelComponents.COLOR_RGB
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBX
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBA
			GL_RGB, GL_RGB_INTEGER, // PixelComponents.COLOR_RGB_sRGB
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBX_sRGB
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBA_sRGB
	};
	
	// Non-integral vs. integral.
	private static int[] s_rgbaLuminanceFormats = {
			GL_LUMINANCE, GL_LUMINANCE_INTEGER_EXT, // PixelComponents.COLOR_RED
			GL_LUMINANCE_ALPHA, GL_LUMINANCE_ALPHA_INTEGER_EXT, // PixelComponents.COLOR_RG
			GL_RGB, GL_RGB_INTEGER, // PixelComponents.COLOR_RGB
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBX
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBA
			GL_RGB, GL_RGB_INTEGER, // PixelComponents.COLOR_RGB_sRGB
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBX_sRGB
			GL_RGBA, GL_RGBA_INTEGER, // PixelComponents.COLOR_RGBA_sRGB
	};
	
	private static PixelDataType getDataType(ImageFormat imageFormat, int forceConvertBits) {
		boolean bForceIntegral = (forceConvertBits & ForcedConvertFlags.FORCE_INTEGRAL_FMT) != 0;
		boolean bForceSigned = (forceConvertBits & ForcedConvertFlags.FORCE_SIGNED_FMT) != 0;
		
		if(!bForceIntegral && !bForceSigned) {
			return imageFormat.getPixelDataType();
		}
		
		switch(imageFormat.getPixelDataType()) {
			case NORM_UNSIGNED_INTEGER:
				if(bForceIntegral) {
					if(bForceSigned) {
						return PixelDataType.SIGNED_INTEGRAL;
					} else {
						return PixelDataType.UNSIGNED_INTEGRAL;
					}
				} else {
					return PixelDataType.NORM_SIGNED_INTEGER;
				}
			case NORM_SIGNED_INTEGER:
				if(bForceIntegral) {
					return PixelDataType.SIGNED_INTEGRAL;
				}
				break;
			
			case UNSIGNED_INTEGRAL:
				if(bForceSigned) {
					return PixelDataType.SIGNED_INTEGRAL;
				}
				break;
			
			case COMPRESSED_UNSIGNED_BC4:
				if(bForceSigned) {
					return PixelDataType.COMPRESSED_SIGNED_BC4;
				}
			case COMPRESSED_UNSIGNED_BC5:
				if(bForceSigned) {
					return PixelDataType.COMPRESSED_SIGNED_BC5;
				}
				break;
			
			default:
				break;
		}
		
		return imageFormat.getPixelDataType();
	}
	
	private static int getOpenGLType(ImageFormat imageFormat, PixelDataType pixelDataType) {
		switch(imageFormat.getBitDepth()) {
			case COMPRESSED:
				return 0xFFFFFFFF;
				
			case PER_COMP_8:
				if(isTypeSigned(pixelDataType)) {
					return GL_BYTE;
				} else {
					return GL_UNSIGNED_BYTE;
				}
				
			case PER_COMP_16:
				if(pixelDataType == PixelDataType.FLOAT) {
					throwIfHalfFloatNotSupported();
					return GL_HALF_FLOAT;
				} else {
					if(isTypeSigned(pixelDataType)) {
						return GL_SHORT;
					} else {
						return GL_UNSIGNED_SHORT;
					}
				}
				
			case PER_COMP_32:
				if(pixelDataType == PixelDataType.FLOAT) {
					throwIfFloatNotSupported();
					return GL_FLOAT;
				} else {
					if(isTypeSigned(pixelDataType)) {
						return GL_INT;
					} else {
						return GL_UNSIGNED_INT;
					}
				}
				
			default:
				int typeIndex = imageFormat.getBitDepth().ordinal() - BitDepth.NUM_PER_COMPONENT.ordinal();
				
				if(!((0 <= typeIndex) && (typeIndex < s_packedTypes.length))) {
					throw new ImageFormatUnsupportedException("Couldn't get the GL type field, due to the bitdepth being outside the packed type array.");
				}
				
				int testType = s_packedTypes[typeIndex];
				
				// Test for implemented features.
				switch(testType) {
					case GL_UNSIGNED_INT_10F_11F_11F_REV:
						throwIfPackedFloatNotSupported();
						break;
					
					case GL_UNSIGNED_INT_5_9_9_9_REV:
						throwIfSharedExpNotSupported();
						break;
				}
				
				return testType;
		}
	}
	
	private static int getOpenGLFormat(ImageFormat imageFormat, PixelDataType pixelDataType, int forceConvertBits) {
		if(imageFormat.getPixelComponents() == PixelComponents.DEPTH) {
			throwIfDepthNotSupported();
			return GL_DEPTH_COMPONENT;
		}
		
		if(imageFormat.getPixelComponents() == PixelComponents.DEPTH_X) {
			throwIfDepthStencilNotSupported();
			return GL_DEPTH_STENCIL;
		}
		
		if(isTypeIntegral(pixelDataType)) {
			throwIfIntegralNotSupported();
		}
		
		int arrayOffset = isTypeIntegral(pixelDataType) ? 1 : 0;
		
		if(imageFormat.getComponentOrder() == ComponentOrder.BGRA) {
			int formatIndex = imageFormat.getPixelComponents().ordinal() - PixelComponents.COLOR_RGB.ordinal();
			formatIndex *= 2;
			
			if(!((0 <= formatIndex) && (formatIndex < s_bgraFormats.length))) {
				throw new ImageFormatUnsupportedException("Couldn't get the GL format field with ORDER_BGRA, due to the order being outside the bgraFormats array.");
			}
			
			return s_bgraFormats[formatIndex];
		} else {
			int formatIndex = imageFormat.getPixelComponents().ordinal();
			formatIndex *= 2;
			
			if(!((0 <= formatIndex) && (formatIndex < s_rgbaFormats.length))) {
				throw new ImageFormatUnsupportedException("Couldn't get the GL format field with ORDER_RGBA, due to the order being outside the rgbaFormats array.");
			}
			
			boolean isRGChannel = getComponentCount(imageFormat) < 3;
			
			if(useLAInsteadOfRG(forceConvertBits)) {
				if(isRGChannel && arrayOffset == 1) {
					throwIfEXT_IntegralNotSupported();
				}
				
				if(isRGChannel) {
					throwIfLANotSupported();
				}
				
				return s_rgbaLuminanceFormats[formatIndex];
			} else {
				if(isRGChannel) {
					throwIfRGNotSupported();
				}
				
				return s_rgbaFormats[formatIndex];
			}
		}
	}
	
	private static boolean isTypeSigned(PixelDataType pixelDataType) {
		PixelDataType[] signedIntegerFormats = { PixelDataType.SIGNED_INTEGRAL, PixelDataType.NORM_SIGNED_INTEGER };
		
		return DdsUtil.isOneOfThese(pixelDataType, signedIntegerFormats);
	}
	
	private static boolean isTypeIntegral(PixelDataType pixelDataType) {
		PixelDataType[] integralIntegerFormats = { PixelDataType.SIGNED_INTEGRAL, PixelDataType.UNSIGNED_INTEGRAL };
		
		return DdsUtil.isOneOfThese(pixelDataType, integralIntegerFormats);
	}
	
	private static int getComponentCount(ImageFormat imageFormat) {
		PixelComponents[] twoCompFormats = { PixelComponents.COLOR_RG, PixelComponents.DEPTH_X };
		PixelComponents[] threeCompFormats = { PixelComponents.COLOR_RGB, PixelComponents.COLOR_RGB_SRGB };
		PixelComponents[] fourCompFormats = { PixelComponents.COLOR_RGBX, PixelComponents.COLOR_RGBA,
				PixelComponents.COLOR_RGBX_SRGB, PixelComponents.COLOR_RGBA_SRGB };
		
		if(DdsUtil.isOneOfThese(imageFormat.getPixelComponents(), twoCompFormats)) {
			return 2;
		}
		
		if(DdsUtil.isOneOfThese(imageFormat.getPixelComponents(), threeCompFormats)) {
			return 3;
		}
		
		if(DdsUtil.isOneOfThese(imageFormat.getPixelComponents(), fourCompFormats)) {
			return 4;
		}
		
		return 1;
	}
	
	private static boolean useLAInsteadOfRG(int forceConvertBits) {
		if((forceConvertBits & ForcedConvertFlags.FORCE_LUMINANCE_FMT) != 0) {
			return true;
		}
		
		try {
			throwIfRGNotSupported();
		} catch(ImageFormatUnsupportedException e) {
			return true;
		}
		
		return false;
	}
	
	private static boolean isArrayTexture(ImageSet imageSet, int forceConvertBits) {
		// No such thing as 3D array textures.
		return imageSet.getDimensions().numDimensions != 3 && ((forceConvertBits & ForcedConvertFlags.FORCE_ARRAY_TEXTURE) != 0 || imageSet.getArrayCount() > 1);
	}
	
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */
	
	private static class ImageFormatUnsupportedException extends RuntimeException {
		public ImageFormatUnsupportedException() {
			super("The image format is not supported by this OpenGL implementation.");
		}
		
		public ImageFormatUnsupportedException(String message) {
			super(message);
		}
		
		private static final long serialVersionUID = 6252749121362296508L;
	}
	
	private static void throwIfS3TCNotSupported() {
		if(!GLContext.getCapabilities().GL_EXT_texture_compression_s3tc) {
			throw new ImageFormatUnsupportedException("S3TC not supported.");
		}
	}
	
	private static void throwIfLATCNotSupported() {
		if(!GLContext.getCapabilities().GL_EXT_texture_compression_latc) {
			throw new ImageFormatUnsupportedException("LATC not supported.");
		}
	}
	
	private static void throwIfRGTCNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_ARB_texture_compression_rgtc || !GLContext.getCapabilities().GL_EXT_texture_compression_rgtc) {
				throw new ImageFormatUnsupportedException("RGTC, part of GL 3.0 and above, is not supported.");
			}
		}
	}
	
	private static void throwIfBPTCNotSupported() {
		if(!GLContext.getCapabilities().GL_ARB_texture_compression_bptc) {
			throw new ImageFormatUnsupportedException("PBTC not supported.");
		}
	}
	
	private static void throwIfSRGBNotSupported() {
		if(!GLContext.getCapabilities().OpenGL21) {
			if(!GLContext.getCapabilities().GL_EXT_texture_sRGB) {
				throw new ImageFormatUnsupportedException("sRGB textures not supported.");
			}
		}
	}
	
	private static void throwIfEXT_SRGBNotSupported() {
		if(!GLContext.getCapabilities().GL_EXT_texture_sRGB) {
			throw new ImageFormatUnsupportedException("sRGB and S3TC textures not supported.");
		}
	}
	
	private static void throwIfSharedExpNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_EXT_texture_shared_exponent) {
				throw new ImageFormatUnsupportedException("Shared exponent texture format not supported.");
			}
		}
	}
	
	private static void throwIfFloatNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_ARB_texture_float) {
				throw new ImageFormatUnsupportedException("Float textures not supported.");
			}
		}
	}
	
	private static void throwIfEXT_FloatNotSupported() {
		if(!GLContext.getCapabilities().GL_ARB_texture_float) {
			throw new ImageFormatUnsupportedException("ARB Float textures not supported.");
		}
	}
	
	private static void throwIfHalfFloatNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_ARB_half_float_pixel) {
				throw new ImageFormatUnsupportedException("Half floats textures not supported.");
			}
		}
	}
	
	private static void throwIfSnormNotSupported() {
		if(!GLContext.getCapabilities().OpenGL31) {
			if(!GLContext.getCapabilities().GL_EXT_texture_snorm) {
				throw new ImageFormatUnsupportedException("Signed normalized textures not supported.");
			}
		}
	}
	
	private static void throwIfEXT_SnormNotSupported() {
		if(!GLContext.getCapabilities().GL_EXT_texture_snorm) {
			throw new ImageFormatUnsupportedException("Signed normalized texture extension not supported.");
		}
	}
	
	private static void throwIfPackedFloatNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_EXT_packed_float) {
				throw new ImageFormatUnsupportedException("Packed 11, 11, 10 float textures not supported.");
			}
		}
	}
	
	private static void throwIfIntegralNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_EXT_texture_integer) {
				throw new ImageFormatUnsupportedException("Integral textures not supported.");
			}
		}
	}
	
	private static void throwIfEXT_IntegralNotSupported() {
		if(!GLContext.getCapabilities().GL_EXT_texture_integer) {
			throw new ImageFormatUnsupportedException("Integral texture extension not supported.");
		}
	}
	
	private static void throwIfRGNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_ARB_texture_rg) {
				throw new ImageFormatUnsupportedException("RG textures not supported.");
			}
		}
	}
	
	private static void throwIfLANotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			return;
		}
		
		if(!GLContext.getCapabilities().OpenGL32) {
			if(!GLContext.getCapabilities().GL_ARB_compatibility) {
				throw new ImageFormatUnsupportedException("Core OpenGL contexts cannot use Luminance/alpha.");
			}
		}
		else {
			int profileMask = glGetInteger(GL32.GL_CONTEXT_PROFILE_MASK);
			
			if((profileMask & GL32.GL_CONTEXT_CORE_PROFILE_BIT) != 0) {
				throw new ImageFormatUnsupportedException("Core OpenGL contexts cannot use Luminance/alpha.");
				
			}
		}
	}
	
	private static void throwIfForceRendertarget(int forceConvertBits) {
		if((forceConvertBits & ForcedConvertFlags.FORCE_COLOR_RENDERABLE_FMT) != 0) {
			throw new CannotForceRenderTargetException();
		}
	}
	
	private static void throwIfDepthNotSupported() {
		if(!GLContext.getCapabilities().OpenGL14) { // Yes, really. Depth textures are old.
			if(!GLContext.getCapabilities().GL_ARB_depth_texture) {
				throw new ImageFormatUnsupportedException("Depth textures not supported.");
			}
		}
	}
	
	private static void throwIfDepthStencilNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_EXT_packed_depth_stencil || !GLContext.getCapabilities().GL_ARB_framebuffer_object) {
				throw new ImageFormatUnsupportedException("Depth/stencil textures not supported.");
			}
		}
	}
	
	private static void throwIfDepthFloatNotSupported() {
		if(!GLContext.getCapabilities().OpenGL30) {
			if(!GLContext.getCapabilities().GL_NV_depth_buffer_float) {
				throw new ImageFormatUnsupportedException("Floating-point depth buffers not supported.");
			}
		}
	}
	
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */
	
	// Ordered by BaseDataFormat*2. The first is 16-bit, the second is 32-bit.
	private static int[] s_floatFormats = {
			GL_R16F,
			GL_R32F,
			GL_RG16F,
			GL_RG32F,
			GL_RGB16F,
			GL_RGB32F,
			GL_RGB16F,
			GL_RGB32F,
			GL_RGBA16F,
			GL_RGBA32F,
			0, // sRGB
			0, // sRGB
			0, // sRGB
			0, // sRGB
			0, // sRGB
			0, // sRGB
			0, // 16-bit float depth buffer.
			GL_DEPTH_COMPONENT32F,
			0, // UNorm+stencil.
	};
	
	// Ordered by number of components * size (8, 16=>0, 1, 2).
	private static int[] s_signedNormFormats = {
			EXTTextureSnorm.GL_R8_SNORM, EXTTextureSnorm.GL_R16_SNORM,
			EXTTextureSnorm.GL_RG8_SNORM, EXTTextureSnorm.GL_RG16_SNORM,
			EXTTextureSnorm.GL_RGB8_SNORM, EXTTextureSnorm.GL_RGB16_SNORM,
			EXTTextureSnorm.GL_RGBA8_SNORM, EXTTextureSnorm.GL_RGBA16_SNORM,
	};
	
	// Ordered by number of components * size (8, 16, 32=>0, 1, 2).
	private static int[] s_signIntegralFormats = {
			GL_R8I, GL_R16I, GL_R32I,
			GL_RG8I, GL_RG16I, GL_RG32I,
			GL_RGB8I, GL_RGB16I, GL_RGB32I,
			GL_RGBA8I, GL_RGBA16I, GL_RGBA32I,
	};
	
	// Ordered by number of components * size (8, 16, 32=>0, 1, 2).
	private static int[] s_unsignIntegralFormats = {
			GL_R8UI, GL_R16UI, GL_R32UI,
			GL_RG8UI, GL_RG16UI, GL_RG32UI,
			GL_RGB8UI, GL_RGB16UI, GL_RGB32UI,
			GL_RGBA8UI, GL_RGBA16UI, GL_RGBA32UI,
	};
	
	private static int getStandardOpenGLFormat(ImageFormat imageFormat, int forceConvertBits) {
		PixelDataType pixelDataType = getDataType(imageFormat, forceConvertBits);
		
		switch(pixelDataType) {
			case NORM_UNSIGNED_INTEGER:
				// Only 16-bit for non Depth_x.
				if(imageFormat.getPixelComponents() == PixelComponents.DEPTH) {
					throwIfDepthNotSupported();
					return GL_DEPTH_COMPONENT16;
				}
				
				// Only 24x8 for this.
				if(imageFormat.getPixelComponents() == PixelComponents.DEPTH_X) {
					throwIfDepthStencilNotSupported();
					return GL_DEPTH24_STENCIL8;
				}
				
				// Color formats.
				if(isSRGBFormat(imageFormat, forceConvertBits)) {
					throwIfSRGBNotSupported();
					
					if(getComponentCount(imageFormat) == 3) {
						return GL_SRGB8;
					} else {
						return GL_SRGB8_ALPHA8;
					}
				}
				
				switch(imageFormat.getBitDepth()) {
					case PER_COMP_8: {
						int components[] = { GL_R8, GL_RG8, GL_RGB8, GL_RGBA8 };
						int numComponents = getComponentCount(imageFormat);
						
						return components[numComponents - 1];
					}
					
					case PER_COMP_16: {
						int components[] = { GL_R16, GL_RG16, GL_RGB16, GL_RGBA16 };
						int numComponents = getComponentCount(imageFormat);
						
						return components[numComponents - 1];
					}
					
					case PACKED_16_BIT_565:
					case PACKED_16_BIT_565_REV:
						return GL_RGB8;
						
					case PACKED_16_BIT_5551:
					case PACKED_16_BIT_1555_REV:
						return GL_RGB5_A1;
						
					case PACKED_16_BIT_4444:
					case PACKED_16_BIT_4444_REV:
						return GL_RGBA4;
						
					case PACKED_32_BIT_8888:
					case PACKED_32_BIT_8888_REV:
						return GL_RGBA8;
						
					case PACKED_32_BIT_1010102:
					case PACKED_32_BIT_2101010_REV:
						return GL_RGB10_A2;
						
					default:
						break;
				}
				
				throw new ImageFormatUnsupportedException("Unisgned normalize integer doesn't match accepted bitdepths.");
				
			case NORM_SIGNED_INTEGER: {
				throwIfSnormNotSupported();
				throwIfForceRendertarget(forceConvertBits);
				
				int numComponents = getComponentCount(imageFormat);
				int compSize = perComponentSize(imageFormat);
				compSize /= 16; // map to 0, 1.
				
				int index = ((numComponents - 1) * 2) + compSize;
				
				return s_signedNormFormats[index];
			}
			
			case UNSIGNED_INTEGRAL:
			case SIGNED_INTEGRAL: {
				throwIfIntegralNotSupported();
				
				int numComponents = getComponentCount(imageFormat);
				int compSize = perComponentSize(imageFormat);
				compSize /= 16; // map to 0, 1, 2.
				
				int index = ((numComponents - 1) * 3) + compSize;
				
				if(pixelDataType == PixelDataType.SIGNED_INTEGRAL) {
					return s_signIntegralFormats[index];
				} else {
					return s_unsignIntegralFormats[index];
				}
			}
			
			case FLOAT: {
				throwIfFloatNotSupported();
				
				if(imageFormat.getBitDepth().ordinal() < BitDepth.NUM_PER_COMPONENT.ordinal()) {
					int offset = 0;
					if(imageFormat.getBitDepth() == BitDepth.PER_COMP_32) {
						offset = 1;
					} else {
						throwIfHalfFloatNotSupported();
					}
					
					if(imageFormat.getPixelComponents() == PixelComponents.DEPTH) {
						throwIfDepthFloatNotSupported();
					}
					
					return throwInvalidFormatIfZero(s_floatFormats[(2 * imageFormat.getPixelComponents().ordinal()) + offset]);
				} else {
					// Only one packed format.
					throwIfPackedFloatNotSupported();
					return GL_R11F_G11F_B10F;
				}
			}
			
			case SHARED_EXP_FLOAT:
				throwIfSharedExpNotSupported();
				throwIfForceRendertarget(forceConvertBits);
				return GL_RGB9_E5;
				
			case COMPRESSED_BC1: {
				throwIfS3TCNotSupported();
				
				if(isSRGBFormat(imageFormat, forceConvertBits)) {
					throwIfEXT_SRGBNotSupported();
					
					if((forceConvertBits & ForcedConvertFlags.FORCE_BC1_ALPHA_FMT) != 0) {
						return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
					} else {
						if(getComponentCount(imageFormat) == 3) {
							return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT;
						} else {
							return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT;
						}
					}
				} else {
					if((forceConvertBits & ForcedConvertFlags.FORCE_BC1_ALPHA_FMT) != 0) {
						return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
					} else {
						if(getComponentCount(imageFormat) == 3) {
							return GL_COMPRESSED_RGB_S3TC_DXT1_EXT;
						} else {
							return GL_COMPRESSED_RGBA_S3TC_DXT1_EXT;
						}
					}
				}
			}
			
			case COMPRESSED_BC2:
				throwIfS3TCNotSupported();
				
				if(isSRGBFormat(imageFormat, forceConvertBits)) {
					throwIfEXT_SRGBNotSupported();
					return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT;
				} else {
					return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT;
				}
				
			case COMPRESSED_BC3:
				throwIfS3TCNotSupported();
				
				if(isSRGBFormat(imageFormat, forceConvertBits)) {
					throwIfEXT_SRGBNotSupported();
					return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT;
				} else {
					return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT;
				}
				
			case COMPRESSED_UNSIGNED_BC4:
				return GL_COMPRESSED_RED_RGTC1;
				
			case COMPRESSED_SIGNED_BC4:
				return GL_COMPRESSED_SIGNED_RED_RGTC1;
				
			case COMPRESSED_UNSIGNED_BC5:
				throw new RuntimeException("No such constant in LWJGL :(");
				// return GL_COMPRESSED_RG_RGTC2;
				
			case COMPRESSED_SIGNED_BC5:
				throw new RuntimeException("No such constant in LWJGL :(");
				// return GL_COMPRESSED_SIGNED_RG_RGTC2;
				
			case COMPRESSED_UNSIGNED_BC6H:
				throwIfBPTCNotSupported();
				return GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB;
				
			case COMPRESSED_SIGNED_BC6H:
				throwIfBPTCNotSupported();
				return GL_COMPRESSED_RGB_BPTC_UNSIGNED_FLOAT_ARB;
				
			case COMPRESSED_BC7:
				throwIfBPTCNotSupported();
				
				if(isSRGBFormat(imageFormat, forceConvertBits)) {
					return GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM_ARB;
				} else {
					return GL_COMPRESSED_RGBA_BPTC_UNORM_ARB;
				}
				
			default:
				break;
		}
		
		throw new ImageFormatUnsupportedException("???");
	}
	
	private static boolean isSRGBFormat(ImageFormat imageFormat, int forceConvertBits) {
		PixelComponents[] srgbFormats = { PixelComponents.COLOR_RGB_SRGB, PixelComponents.COLOR_RGBX_SRGB,
				PixelComponents.COLOR_RGBA_SRGB };
		
		if(DdsUtil.isOneOfThese(imageFormat.getPixelComponents(), srgbFormats)) {
			return true;
		}
		
		if((forceConvertBits & ForcedConvertFlags.FORCE_SRGB_COLORSPACE_FMT) == 0) {
			return false;
		}
		
		PixelDataType[] srgbTypes = { PixelDataType.NORM_UNSIGNED_INTEGER, PixelDataType.COMPRESSED_BC1,
				PixelDataType.COMPRESSED_BC2, PixelDataType.COMPRESSED_BC3, PixelDataType.COMPRESSED_BC7 };
		
		if(DdsUtil.isOneOfThese(imageFormat.getPixelDataType(), srgbTypes)) {
			if(imageFormat.getPixelDataType() != PixelDataType.NORM_UNSIGNED_INTEGER) {
				return true;
			}
		} else {
			return false;
		}
		
		// Unsigned normalized integers. Check for RGB or RGBA components.
		PixelComponents convertableFormats[] = { PixelComponents.COLOR_RGB, PixelComponents.COLOR_RGBX, PixelComponents.COLOR_RGBA };
		
		return DdsUtil.isOneOfThese(imageFormat.getPixelComponents(), convertableFormats);
	}
	
	private static int perComponentSize(ImageFormat imageFormat) {
		switch(imageFormat.getBitDepth()) {
			case PER_COMP_8:
				return 8;
			case PER_COMP_16:
				return 16;
			case PER_COMP_32:
				return 32;
			default:
				return -1;
		}
	}
	
	private static int throwInvalidFormatIfZero(int input) {
		if(input == 0) {
			throw new ImageFormatUnsupportedException();
		}
		
		return input;
	}
	
	private static int LARG_COMPRESSED_CONV(boolean bConvertToLA, int oldformat, int newFormat) {
		if(bConvertToLA) {
			throwIfLATCNotSupported();
			return newFormat;
		} else {
			throwIfRGTCNotSupported();
		}
		
		return oldformat;
	}
	
	private static int LARG_INTEGRAL_CONV(boolean bConvertToLA, int oldformat, int newFormat) {
		if(bConvertToLA) {
			throwIfEXT_IntegralNotSupported();
			return newFormat;
		} else {
			throwIfRGNotSupported();
		}
		
		return oldformat;
	}
	
	private static int LARG_FLOAT_CONV(boolean bConvertToLA, int oldformat, int newFormat) {
		if(bConvertToLA) {
			throwIfEXT_FloatNotSupported();
			return newFormat;
		} else {
			throwIfRGNotSupported();
		}
		
		return oldformat;
	}
	
	private static int LARG_SNORM_CONV(boolean bConvertToLA, int oldformat, int newFormat) {
		if(bConvertToLA) {
			throwIfEXT_SnormNotSupported();
			return newFormat;
		} else {
			throwIfRGNotSupported();
		}
		
		return oldformat;
	}
	
	private static int LARG_NORM_CONV(boolean bConvertToLA, int oldformat, int newFormat) {
		if(bConvertToLA) {
			throwIfLANotSupported();
			return newFormat;
		} else {
			throwIfRGNotSupported();
		}
		
		return oldformat;
	}
	
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 */
	
	private static class TextureBinder {
		
		@Override
		protected void finalize() throws Throwable {
			super.finalize();
			
			if(m_texture != 0) {
				glBindTexture(m_texTarget, 0);
			}
		}
		
		void bind(int textureTarget, int texture) {
			m_texture = texture;
			m_texTarget = textureTarget;
			glBindTexture(m_texTarget, m_texture);
		}
		
		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 */
		
		private int m_texture;
		private int m_texTarget;
	}
	
	private static class CannotForceTextureStorage extends RuntimeException {
		private static final long serialVersionUID = -3344749448213535867L;
		
		private CannotForceTextureStorage() {
			super("The current OpenGL implementation does not support ARB_texture_storage or GL 4.2 or above.");
		}
	}
	
	private static class CannotForceDSAUsage extends RuntimeException {
		private static final long serialVersionUID = 7928385809263161372L;
		
		private CannotForceDSAUsage() {
			super("The current OpenGL implementation does not support EXT_direct_state_access.");
		}
	}
	
	private static class CannotForceRenderTargetException extends RuntimeException {
		private static final long serialVersionUID = 652152072608548652L;
		
		private CannotForceRenderTargetException() {
			super("The image format cannot be forced to be a renderable format without compromising the data.");
		}
	}
	
	private static void createTexture(int textureName, ImageSet imageSet, int forceConvertBits) {
		if((forceConvertBits & ForcedConvertFlags.FORCE_TEXTURE_STORAGE) != 0) {
			if(!isTextureStorageSupported()) {
				throw new CannotForceTextureStorage();
			}
			
			forceConvertBits |= ForcedConvertFlags.USE_TEXTURE_STORAGE;
		}
		
		if((forceConvertBits & ForcedConvertFlags.USE_TEXTURE_STORAGE) != 0) {
			if(!isTextureStorageSupported()) {
				forceConvertBits &= ~ForcedConvertFlags.USE_TEXTURE_STORAGE;
			}
		}
		
		if((forceConvertBits & ForcedConvertFlags.FORCE_DSA) != 0) {
			if(!isDirectStateAccessSupported()) {
				throw new CannotForceDSAUsage();
			}
			
			forceConvertBits |= ForcedConvertFlags.USE_DSA;
		}
		
		if((forceConvertBits & ForcedConvertFlags.USE_DSA) != 0) {
			if(!isDirectStateAccessSupported()) {
				forceConvertBits &= ~ForcedConvertFlags.USE_DSA;
			}
		}
		
		ImageFormat imageFormat = imageSet.getFormat();
		int internalFormat = getInternalFormat(imageFormat, forceConvertBits);
		OpenGLPixelTransferParams upload = getUploadFormatType(imageFormat, forceConvertBits);
		
		switch(getTextureType(imageSet, forceConvertBits)) {
			case GL_TEXTURE_2D:
				build2DTexture(textureName, imageSet, forceConvertBits, internalFormat, upload);
				break;
			
			default:
				throw new IllegalArgumentException("Only GL_TEXTURE_2D supported.");
		}
	}
	
	private static boolean isTextureStorageSupported() {
		if(!GLContext.getCapabilities().OpenGL42) {
			if(!GLContext.getCapabilities().GL_ARB_texture_storage) {
				return false;
			}
		}
		
		return true;
	}
	
	private static boolean isDirectStateAccessSupported() {
		return GLContext.getCapabilities().GL_EXT_direct_state_access;
	}
	
	private static void build2DTexture(int textureName, ImageSet imageSet,
			int forceConvertBits, int internalFormat,
			OpenGLPixelTransferParams upload) {
		setupUploadState(imageSet.getFormat());
		TextureBinder textureBinder = new TextureBinder();
		
		if((forceConvertBits & ForcedConvertFlags.USE_DSA) == 0) {
			textureBinder.bind(GL_TEXTURE_2D, textureName);
			textureName = 0;
		}
		
		int numMipmaps = imageSet.getMipmapCount();
		
		texStorageBase(GL_TEXTURE_2D, forceConvertBits, imageSet.getDimensions(),
				numMipmaps, internalFormat, upload, textureName);
		
		for(int mipmapLevel = 0; mipmapLevel < numMipmaps; mipmapLevel++) {
			Dimensions imageDimensions = imageSet.getDimensions(mipmapLevel);
			ByteBuffer imageDataBuffer = imageSet.getImageData(mipmapLevel, 0, 0);
			
			texSubImage(textureName, GL_TEXTURE_2D, mipmapLevel, internalFormat, imageDimensions, upload,
					imageDataBuffer);
		}
		
		finalizeTexture(textureName, GL_TEXTURE_2D, imageSet);
	}
	
	private static void setupUploadState(ImageFormat imageFormat) {
		glPixelStorei(GL_UNPACK_SWAP_BYTES, GL_FALSE);
		glPixelStorei(GL_UNPACK_LSB_FIRST, GL_FALSE);
		glPixelStorei(GL_UNPACK_ROW_LENGTH, 0);
		glPixelStorei(GL_UNPACK_SKIP_ROWS, 0);
		glPixelStorei(GL_UNPACK_SKIP_PIXELS, 0);
		glPixelStorei(GL_UNPACK_IMAGE_HEIGHT, 0);
		glPixelStorei(GL_UNPACK_SKIP_IMAGES, 0);
		glPixelStorei(GL_UNPACK_ALIGNMENT, imageFormat.getLineAlignment());
	}
	
	// Works for just 1D/2D/3D.
	private static void texStorageBase(int textureTarget, int forceConvertBits,
			Dimensions imageDimensions, int numMipmaps, int internalFormat,
			OpenGLPixelTransferParams upload, int textureName) {
		if((forceConvertBits & ForcedConvertFlags.USE_TEXTURE_STORAGE) != 0) {
			DdsUtil.throwNotYetPortedException();
		} else {
			manTexStorageBase(textureName, textureTarget, imageDimensions,
					numMipmaps, internalFormat, upload);
		}
	}
	
	// Only works for TEXTURE_1D, 2D, and 3D.
	// DSA-style
	private static void manTexStorageBase(int texture, int textureTarget,
			Dimensions imageDimensions, int numMipmaps, int internalFormat,
			OpenGLPixelTransferParams upload) {
		// Zero means bound, so no DSA.
		if(texture == 0) {
			manTexStorageBase(textureTarget, imageDimensions, numMipmaps,
					internalFormat, upload);
			return;
		}
		
		DdsUtil.throwNotYetPortedException();
	}
	
	private static void manTexStorageBase(int textureTarget,
			Dimensions imageDimensions, int numMipmaps, int internalFormat,
			OpenGLPixelTransferParams upload) {
		for(int mipmap = 0; mipmap < numMipmaps; mipmap++) {
			Dimensions levelDims = DdsUtil.calcMipmapLevelDimensions(imageDimensions, mipmap);
			
			switch(imageDimensions.numDimensions) {
				case 1:
					DdsUtil.throwNotYetPortedException();
					break;
				
				case 2:
					glTexImage2D(textureTarget, mipmap, internalFormat,
							levelDims.width, levelDims.height, 0,
							upload.format, upload.type, (ByteBuffer)null);
					break;
				
				case 3:
					DdsUtil.throwNotYetPortedException();
					break;
			}
		}
	}
	
	private static void texSubImage(int texture, int textureTarget,
			int mipmapLevel, int internalFormat, Dimensions imageDimensions,
			OpenGLPixelTransferParams upload, ByteBuffer imageData) {
		// Zero means bound, so no DSA.
		if(texture == 0) {
			texSubImage(textureTarget, mipmapLevel, internalFormat,
					imageDimensions, upload, imageData);
			return;
		}
		
		DdsUtil.throwNotYetPortedException();
	}
	
	private static void texSubImage(int textureTarget, int mipmapLevel,
			int internalFormat, Dimensions imageDimensions,
			OpenGLPixelTransferParams upload, ByteBuffer imageData) {
		switch(imageDimensions.numDimensions) {
			case 1:
				if(upload.blockByteCount != 0) {
					glCompressedTexSubImage1D(textureTarget, mipmapLevel, 0,
							imageDimensions.width, internalFormat, imageData);
				} else {
					glTexSubImage1D(textureTarget, mipmapLevel, 0,
							imageDimensions.width, upload.format, upload.type,
							imageData);
				}
				break;
			
			case 2:
				if(upload.blockByteCount != 0) {
					glCompressedTexSubImage2D(textureTarget, mipmapLevel, 0, 0,
							imageDimensions.width, imageDimensions.height,
							internalFormat, imageData);
				} else {
					glTexSubImage2D(textureTarget, mipmapLevel, 0, 0,
							imageDimensions.width, imageDimensions.height,
							upload.format, upload.type, imageData);
				}
				break;
			
			case 3:
				if(upload.blockByteCount != 0) {
					glCompressedTexSubImage3D(textureTarget, mipmapLevel, 0, 0, 0,
							imageDimensions.width, imageDimensions.height,
							imageDimensions.depth, internalFormat, imageData);
				} else {
					glTexSubImage3D(textureTarget, mipmapLevel, 0, 0, 0,
							imageDimensions.width, imageDimensions.height,
							imageDimensions.depth, upload.format, upload.type,
							imageData);
				}
				break;
		}
	}
	
	private static void finalizeTexture(int texture, int textureTarget, ImageSet imageSet) {
		// Zero means bound, so no DSA.
		if(texture == 0) {
			finalizeTexture(textureTarget, imageSet);
			return;
		}
		
		DdsUtil.throwNotYetPortedException();
	}
	
	// Texture must be bound to the target.
	private static void finalizeTexture(int textureTarget, ImageSet imageSet) {
		int numMipmaps = imageSet.getMipmapCount();
		
		glTexParameteri(textureTarget, GL_TEXTURE_BASE_LEVEL, 0);
		glTexParameteri(textureTarget, GL_TEXTURE_MAX_LEVEL, numMipmaps - 1);
		
		// Ensure the texture is texture-complete.
		ImageFormat imageFormat = imageSet.getFormat();
		if(isTypeIntegral(imageFormat.getPixelDataType())) {
			glTexParameteri(textureTarget, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
			glTexParameteri(textureTarget, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
		} else {
			glTexParameteri(textureTarget, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
			glTexParameteri(textureTarget, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		}
	}
}
 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 Mesh {
	private int vao;
	private HashMap<String,Integer> vaoMap;
	
	private ArrayList<RenderCmd> renderCommands;
	
	public Mesh(URL url) throws IOException, XmlPullParserException {
		renderCommands = new ArrayList<>();
		
		ArrayList<Attribute> attributes = new ArrayList<>();
		
		ByteBuffer attributeData = BufferUtils.createByteBuffer(0), indexData = BufferUtils.createByteBuffer(0);
		
		class VAO {
			String name;
			ArrayList<Integer> sources;
			
			VAO(String name) {
				this.name = name;
				
				sources = new ArrayList<>();
			}
		}
		
		ArrayList<VAO> vaos = null;
		
		try(InputStream is = url.openStream()) {
			XmlPullParser xml = XmlPullParserFactory.newInstance().newPullParser();
			xml.setInput(is, "UTF-8");
			
			xml.next();
			
			xml.require(XmlPullParser.START_TAG, null, "mesh");
			
			do {
				switch(xml.nextTag()) {
					case XmlPullParser.START_TAG:
						switch(xml.getName()) {
							case "attribute": {
								String index = xml.getAttributeValue(null, "index");
								String type = xml.getAttributeValue(null, "type");
								String size = xml.getAttributeValue(null, "size");
								
								if(index == null)
									throw new IllegalArgumentException("<attribute> missing 'index'");
								if(type == null)
									throw new IllegalArgumentException("<attribute> missing 'type'");
								if(size == null)
									throw new IllegalArgumentException("<attribute> missing 'size'");
								
								Attribute attrib = new Attribute(Integer.parseInt(index), type, Integer.parseInt(size));
								attributes.add(attrib);
								
								xml.next();
								xml.require(XmlPullParser.TEXT, null, null);
								
								attributeData = attrib.storeData(attributeData, clean(xml.getText().trim().replace("\r\n", " ").replace('\n', ' ').split(" ")));
								
								xml.next();
								xml.require(XmlPullParser.END_TAG, null, "attribute");
								
								break;
							}
							case "indices": {
								String primitive = xml.getAttributeValue(null, "cmd");
								String type = xml.getAttributeValue(null, "type");
								
								if(primitive == null)
									throw new IllegalArgumentException("<indices> missing 'cmd'");
								if(type == null)
									throw new IllegalArgumentException("<indices> missing 'type'");
								
								RenderCmd cmd = new RenderCmd(primitive, type);
								renderCommands.add(cmd);
								
								xml.next();
								xml.require(XmlPullParser.TEXT, null, null);
								
								indexData = cmd.storeData(indexData, clean(xml.getText().trim().replace("\r\n", " ").replace('\n', ' ').split(" ")));
								
								xml.next();
								xml.require(XmlPullParser.END_TAG, null, "indices");
								
								break;
							}
							case "vao": {
								if(vaos == null)
									vaos = new ArrayList<>();
								
								String name = xml.getAttributeValue(null, "name");
								
								if(name == null)
									throw new IllegalArgumentException("<vao> missing 'name'");
								
								VAO vao = new VAO(name);
								vaos.add(vao);
								
								while(xml.nextTag() == XmlPullParser.START_TAG) {
									xml.require(XmlPullParser.START_TAG, null, "source");
									
									String attrib = xml.getAttributeValue(null, "attrib");
									
									if(attrib == null)
										throw new IllegalArgumentException("<source> missing 'attrib'");
									
									vao.sources.add(Integer.parseInt(attrib));
									xml.nextTag();
									xml.require(XmlPullParser.END_TAG, null, "source");
								}
								
								xml.require(XmlPullParser.END_TAG, null, "vao");
								
								break;
							}
							case "arrays": {
								String primitive = xml.getAttributeValue(null, "cmd");
								String start = xml.getAttributeValue(null, "start");
								String count = xml.getAttributeValue(null, "count");
								
								if(primitive == null)
									throw new IllegalArgumentException("<arrays> missing 'cmd'");
								if(start == null)
									throw new IllegalArgumentException("<arrays> missing 'start'");
								if(count == null)
									throw new IllegalArgumentException("<arrays> missing 'count'");
								
								RenderCmd cmd = new RenderCmd(primitive, Integer.parseInt(start), Integer.parseInt(count));
								renderCommands.add(cmd);
								
								xml.next();
								xml.require(XmlPullParser.END_TAG, null, "arrays");
								
								break;
							}
							default:
								throw new IllegalArgumentException("Invalid TAG name: " + xml.getName());
						}
						
						break;
				}
			} while(xml.next() != XmlPullParser.END_DOCUMENT);
		}
		
		if(attributes.size() == 0)
			throw new IllegalArgumentException("There must be at least 1 set of attributes.");
		if(renderCommands.size() == 0)
			throw new IllegalArgumentException("There must be at least 1 render command.");
		
		attributeData.flip();
		indexData.flip();
		
		int vbo1 = glGenBuffers();
		glBindBuffer(GL_ARRAY_BUFFER, vbo1);
		glBufferData(GL_ARRAY_BUFFER, (ByteBuffer)BufferUtils.createByteBuffer(attributeData.capacity()).put(attributeData).flip(), GL_STATIC_DRAW);
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		
		int vbo2 = -1;
		if(indexData.hasRemaining()) {
			vbo2 = glGenBuffers();
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo2);
			glBufferData(GL_ELEMENT_ARRAY_BUFFER, (ByteBuffer)BufferUtils.createByteBuffer(indexData.capacity()).put(indexData).flip(), GL_STATIC_DRAW);
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		}
		
		vao = glGenVertexArrays();
		glBindVertexArray(vao);
		
		glBindBuffer(GL_ARRAY_BUFFER, vbo1);
		for(Attribute attrib : attributes) {
			glEnableVertexAttribArray(attrib.index);
			glVertexAttribPointer(attrib.index, attrib.size, attrib.type.dataType, attrib.type.normalized, 0, attrib.offset);
		}
		
		if(vbo2 >= 0)
			glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo2);
		
		glBindVertexArray(0);
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
		
		if(vaos != null) {
			vaoMap = new HashMap<>();
			
			for(VAO vao : vaos) {
				int v = glGenVertexArrays();
				glBindVertexArray(v);
				
				vaoMap.put(vao.name, v);
				
				glBindBuffer(GL_ARRAY_BUFFER, vbo1);
				for(Attribute attrib : attributes) {
					if(vao.sources.contains(attrib.index)) {
						glEnableVertexAttribArray(attrib.index);
						glVertexAttribPointer(attrib.index, attrib.size, attrib.type.dataType, attrib.type.normalized, 0, attrib.offset);
					}
				}
				
				if(vbo2 >= 0)
					glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, vbo2);
				
				glBindVertexArray(0);
				glBindBuffer(GL_ARRAY_BUFFER, 0);
				glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
			}
		}
	}
	
	private String[] clean(String[] data) {
		ArrayList<String> clean = new ArrayList<>();
		for(String s : data)
			if(!(s = s.trim()).isEmpty())
				clean.add(s);
		return clean.toArray(new String[clean.size()]);
	}
	
	public void render() {
		glBindVertexArray(vao);
		for(RenderCmd cmd : renderCommands)
			cmd.render();
		glBindVertexArray(0);
	}
	
	public void render(String name) {
		if(vaoMap == null)
			throw new IllegalArgumentException("vaoMap is null, must call render() method.");
		
		if(!vaoMap.containsKey(name))
			throw new IllegalArgumentException(name + " could not be found.");
		
		glBindVertexArray(vaoMap.get(name));
		for(RenderCmd cmd : renderCommands)
			cmd.render();
		glBindVertexArray(0);
	}
	
	private static class RenderCmd {
		private boolean isIndexedCmd;
		private int primitive;
		private int start;
		private int count;
		private RenderCommandType type;
		
		public RenderCmd(String primitive, String type) {
			isIndexedCmd = true;
			this.primitive = getPrimitive(primitive);
			this.type = RenderCommandType.getRenderType(type);
		}
		
		public RenderCmd(String primitive, int start, int count) {
			isIndexedCmd = false;
			this.primitive = getPrimitive(primitive);
			this.start = start;
			this.count = count;
		}
		
		public ByteBuffer storeData(ByteBuffer b, String[] data) {
			start = b.position();
			count = data.length;
			
			ByteBuffer b2 = BufferUtils.createByteBuffer(start + count * type.size);
			b2.put((ByteBuffer)b.flip());
			type.store(b2, data);
			
			return b2;
		}
		
		private static int getPrimitive(String name) {
			switch(name) {
				case "triangles":
					return GL_TRIANGLES;
				case "tri-fan":
					return GL_TRIANGLE_FAN;
				case "tri-strip":
					return GL_TRIANGLE_STRIP;
				case "lines":
					return GL_LINES;
				case "line-strip":
					return GL_LINE_STRIP;
				case "line-loop":
					return GL_LINE_LOOP;
				case "points":
					return GL_POINTS;
				default:
					throw new IllegalArgumentException("Invalid primitive name: " + name);
			}
		}
		
		public void render() {
			if(isIndexedCmd)
				glDrawElements(primitive, count, type.dataType, start);
			else
				glDrawArrays(primitive, start, count);
		}
	}
	
	private enum RenderCommandType {
		UBYTE("ubyte", GL_UNSIGNED_BYTE, 1) {
			public void store(ByteBuffer b, String[] data) {
				for(String s : data)
					b.put((byte)Long.parseLong(s));
			}
		},
		USHORT("ushort", GL_UNSIGNED_SHORT, 2) {
			public void store(ByteBuffer b, String[] data) {
				for(String s : data)
					b.putShort((short)Long.parseLong(s));
			}
		},
		UINT("uint", GL_UNSIGNED_INT, 4) {
			public void store(ByteBuffer b, String[] data) {
				for(String s : data)
					b.putInt((int)Long.parseLong(s));
			}
		};
		
		private String name;
		private int dataType;
		private int size;
		
		private RenderCommandType(String name, int type, int size) {
			this.name = name;
			this.dataType = type;
			this.size = size;
		}
		
		public abstract void store(ByteBuffer b, String[] data);
		
		public static RenderCommandType getRenderType(String name) {
			for(RenderCommandType rt : values()) {
				if(rt.name.equals(name))
					return rt;
			}
			
			throw new IllegalArgumentException("Unsupported render command type!");
		}
	}
	
	private class Attribute {
		private int index;
		private AttributeType type;
		private int size;
		private int offset;
		
		public Attribute(int index, String type, int size) {
			this.index = index;
			this.size = size;
			
			this.type = AttributeType.getAttributeType(type);
		}
		
		public ByteBuffer storeData(ByteBuffer b, String[] data) {
			offset = b.position();
			
			ByteBuffer b2 = BufferUtils.createByteBuffer(offset + data.length * type.size);
			b2.put((ByteBuffer)b.flip());
			type.store(b2, data);
			
			return b2;
		}
	}
	
	private enum AttributeType {
		FLOAT("float", false, GL_FLOAT, 4),
		INT("int", false, GL_INT, 4), UINT("uint", false, GL_UNSIGNED_INT, 4), NORM_INT("norm-int", true, GL_INT, 4), NORM_UINT("norm-uint", true, GL_UNSIGNED_INT, 4),
		SHORT("short", false, GL_SHORT, 2), USHORT("ushort", false, GL_UNSIGNED_SHORT, 2), NORM_SHORT("norm-short", true, GL_SHORT, 2), NORM_USHORT("norm-ushort", true, GL_UNSIGNED_SHORT, 2),
		BYTE("byte", false, GL_BYTE, 1), UBYTE("ubyte", false, GL_UNSIGNED_BYTE, 1), NORM_BYTE("norm-byte", true, GL_BYTE, 1), NORM_UBYTE("norm-ubyte", true, GL_UNSIGNED_BYTE, 1);
		
		private String name;
		private boolean normalized;
		private int dataType;
		private int size;
		
		private AttributeType(String name, boolean normalized, int type, int size) {
			this.name = name;
			this.normalized = normalized;
			this.dataType = type;
			this.size = size;
		}
		
		public void store(ByteBuffer b, String[] data) {
			switch(dataType) {
				case GL_FLOAT:
					for(String s : data)
						b.putFloat(Float.parseFloat(s));
					break;
				case GL_INT:
				case GL_UNSIGNED_INT:
					for(String s : data)
						b.putInt((int)Long.parseLong(s));
					break;
				case GL_SHORT:
				case GL_UNSIGNED_SHORT:
					for(String s : data)
						b.putShort((short)Long.parseLong(s));
					break;
				case GL_BYTE:
				case GL_UNSIGNED_BYTE:
					for(String s : data)
						b.put((byte)Long.parseLong(s));
					break;
			}
		}
		
		public static AttributeType getAttributeType(String name) {
			for(AttributeType at : values()) {
				if(at.name.equals(name))
					return at;
			}
			
			throw new IllegalArgumentException("Unsupported attribute 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 class Timer {
	public enum Type {
		LOOP,
		SINGLE,
		INFINITE
	}
	
	private Type type;
	private float secDuration;
	
	private boolean hasUpdated;
	private boolean isPaused;
	
	private float absPrevTime;
	private float secAccumTime;
	
	private long elapsedTime;
	
	public Timer() {
		this(Type.INFINITE, 1);
	}
	
	public Timer(Type type, float duration) {
		this.type = type;
		this.secDuration = duration;
		
		if(type != Type.INFINITE && duration <= 0)
			throw new IllegalArgumentException("duration cannot be less than or equal to 0");
	}
	
	public void reset() {
		hasUpdated = false;
		secAccumTime = 0;
	}
	
	public boolean togglePause() {
		isPaused = !isPaused;
		return isPaused;
	}
	
	public boolean isPaused() {
		return isPaused;
	}
	
	public void setPause(boolean pause) {
		isPaused = pause;
	}
	
	public boolean update(long deltaTime) {
		elapsedTime += deltaTime;
		
		float currTime = elapsedTime / (float)1e9;
		
		if(!hasUpdated) {
			absPrevTime = currTime;
			hasUpdated = true;
		}
		
		if(isPaused) {
			absPrevTime = currTime;
			return false;
		}
		
		float delta = currTime - absPrevTime;
		secAccumTime += delta;
		
		absPrevTime = currTime;
		
		if(type == Type.SINGLE)
			return secAccumTime > secDuration;
		
		return false;
	}
	
	public void rewind(float secRewind) {
		secAccumTime -= secRewind;
		if(secAccumTime < 0)
			secAccumTime = 0;
	}
	
	public void fastForward(float secFF) {
		secAccumTime += secFF;
	}
	
	public float getAlpha() {
		switch(type) {
			case LOOP:
				return (secAccumTime % secDuration) / secDuration;
			case SINGLE:
				return Utils.clamp(secAccumTime / secDuration, 0, 1);
			case INFINITE:
			default:
				return -1;
		}
	}
	
	public float getProgression() {
		switch(type) {
			case LOOP:
				return secAccumTime % secDuration;
			case SINGLE:
				return Utils.clamp(secAccumTime, 0, secDuration);
			case INFINITE:
			default:
				return -1;
		}
	}
	
	public float getTimeSinceStart() {
		return secAccumTime;
	}
}
 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;
	}
}
 public class Scene {
	private HashMap<String,Mesh> meshes;
	private HashMap<String,SceneTexture> textures;
	private HashMap<String,SceneProgram> programs;
	private HashMap<String,SceneNode> nodes;
	
	public Scene(URL url, Class<?> clazz, String prefix) throws IOException, XmlPullParserException {
		meshes = new HashMap<>();
		textures = new HashMap<>();
		programs = new HashMap<>();
		nodes = new HashMap<>();
		
		try(InputStream is = url.openStream()) {
			XmlPullParser xml = XmlPullParserFactory.newInstance().newPullParser();
			xml.setInput(is, "UTF-8");
			
			xml.next();
			xml.require(XmlPullParser.START_TAG, null, "scene");
			
			xml.nextTag();
			xml.require(XmlPullParser.START_TAG, null, "mesh");
			
			int count = 0;
			do {
				String id = xml.getAttributeValue(null, "xml:id");
				String file = xml.getAttributeValue(null, "file");
				
				throwIfNull(id, "id", "mesh " + count);
				throwIfNull(file, "file", "mesh " + count);
				
				if(meshes.containsKey(id))
					throw new IllegalArgumentException("Mesh named '" + id + "' already exists.");
				
				try {
					meshes.put(id, new Mesh(clazz.getResource(prefix + file)));
				} catch(Exception exc) {
					throw new IllegalArgumentException("Invalid mesh file for '" + id + "'.", exc);
				}
				
				xml.next();
				xml.require(XmlPullParser.END_TAG, null, "mesh");
				
				count++;
			} while(xml.nextTag() == XmlPullParser.START_TAG && xml.getName().equals("mesh"));
			
			xml.require(XmlPullParser.START_TAG, null, "texture");
			
			count = 0;
			do {
				String id = xml.getAttributeValue(null, "xml:id");
				String file = xml.getAttributeValue(null, "file");
				
				throwIfNull(id, "id", "texture " + count);
				throwIfNull(file, "file", "texture " + count);
				
				String s = xml.getAttributeValue(null, "srgb");
				throwIfNull(s, "srgb", "texture " + count);
				
				boolean srgb;
				try {
					srgb = Boolean.parseBoolean(s);
				} catch(Exception exc) {
					throw new IllegalArgumentException("srgb should be only true or false, invalid value at texture " + count + ".");
				}
				
				if(textures.containsKey(id))
					throw new IllegalArgumentException("Texture named '" + id + "' already exists.");
				
				try {
					textures.put(id, new SceneTexture(clazz.getResourceAsStream(prefix + file), srgb ? ForcedConvertFlags.FORCE_SRGB_COLORSPACE_FMT : 0));
				} catch(Exception exc) {
					throw new IllegalArgumentException("Texture named '" + id + "' is invalid.", exc);
				}
				
				xml.next();
				xml.require(XmlPullParser.END_TAG, null, "texture");
				
				count++;
			} while(xml.nextTag() == XmlPullParser.START_TAG && xml.getName().equals("texture"));
			
			xml.require(XmlPullParser.START_TAG, null, "prog");
			
			count = 0;
			do {
				String id = xml.getAttributeValue(null, "xml:id");
				String vertexFile = xml.getAttributeValue(null, "vert");
				String fragmentFile = xml.getAttributeValue(null, "frag");
				String modelMatrix = xml.getAttributeValue(null, "model-to-camera");
				
				throwIfNull(id, "id", "prog " + count);
				throwIfNull(vertexFile, "vert", "prog " + count);
				throwIfNull(fragmentFile, "frag", "prog " + count);
				throwIfNull(modelMatrix, "model-to-camera", "prog " + count);
				
				String normalModelMatrix = xml.getAttributeValue(null, "normal-model-to-camera");
				String invNormalModelMatrix = xml.getAttributeValue(null, "normal-camera-to-model");
				String geometryFile = xml.getAttributeValue(null, "geom");
				
				if(programs.containsKey(id))
					throw new IllegalArgumentException("Program named '" + id + "' already exists.");
				
				ShaderProgram program;
				try {
					if(geometryFile != null)
						program = new ShaderProgram(Utils.readFully(clazz.getResourceAsStream(prefix + vertexFile)),
								Utils.readFully(clazz.getResourceAsStream(prefix + geometryFile)),
								Utils.readFully(clazz.getResourceAsStream(prefix + fragmentFile)));
					else
						program = new ShaderProgram(Utils.readFully(clazz.getResourceAsStream(prefix + vertexFile)),
								Utils.readFully(clazz.getResourceAsStream(prefix + fragmentFile)));
				} catch(Exception exc) {
					throw new IllegalArgumentException("Invalid shaders for program " + id, exc);
				}
				
				int matrixLocation = program.getUniformLocation(modelMatrix);
				if(matrixLocation == -1)
					throw new IllegalArgumentException("Program shader '" + id + "' does not have a matrix uniform.");
				
				int normalMatrixLocation = -1;
				if(normalModelMatrix != null) {
					normalMatrixLocation = program.getUniformLocation(normalModelMatrix);
					if(normalMatrixLocation == -1)
						throw new IllegalArgumentException("Program shader '" + id + "' does not have a normal matrix uniform.");
				}
				
				int invNormalMatrixLocation = -1;
				if(invNormalModelMatrix != null) {
					invNormalMatrixLocation = program.getUniformLocation(invNormalModelMatrix);
					if(normalMatrixLocation == -1)
						throw new IllegalArgumentException("Program shader '" + id + "' does not have a inverse normal matrix uniform.");
				}
				
				programs.put(id, new SceneProgram(program, matrixLocation, normalMatrixLocation, invNormalMatrixLocation));
				
				ArrayList<String> blocks = new ArrayList<>();
				ArrayList<String> samplers = new ArrayList<>();
				
				while(xml.nextTag() == XmlPullParser.START_TAG) {
					switch(xml.getName()) {
						case "block": {
							String name = xml.getAttributeValue(null, "name");
							String binding = xml.getAttributeValue(null, "binding");
							
							throwIfNull(name, "name", "block in program '" + id + "'");
							throwIfNull(binding, "binding", "block in program '" + id + "'");
							
							if(blocks.contains(name))
								throw new IllegalArgumentException("Block '" + name + "' is used more than once in program '" + id + "'.");
							
							blocks.add(name);
							
							int blockIndex = program.getUniformBlockIndex(name);
							if(blockIndex == GL_INVALID_INDEX)
								throw new IllegalArgumentException("Block '" + name + "' cannot be found in program '" + id + "'.");
							
							int bindPoint;
							try {
								bindPoint = Integer.parseInt(binding);
							} catch(Exception exc) {
								throw new IllegalArgumentException("Binding in block '" + name + "' is invalid value; must be integer.");
							}
							
							glUniformBlockBinding(program.getProgram(), blockIndex, bindPoint);
							
							xml.next();
							xml.require(XmlPullParser.END_TAG, null, "block");
							
							break;
						}
						case "sampler": {
							String name = xml.getAttributeValue(null, "name");
							String texUnit = xml.getAttributeValue(null, "unit");
							
							throwIfNull(name, "name", "sampler in program '" + id + "'");
							throwIfNull(texUnit, "unit", "sampler in program '" + id + "'");
							
							if(samplers.contains(name))
								throw new IllegalArgumentException("Sampler '" + name + "' is used more than once in program '" + id + "'.");
							
							samplers.add(name);
							
							int samplerLocation = program.getUniformLocation(name);
							if(samplerLocation == -1)
								throw new IllegalArgumentException("Sampler '" + name + "' cannot be found in program '" + id + "'.");
							
							int textureUnit;
							try {
								textureUnit = Integer.parseInt(texUnit);
							} catch(Exception exc) {
								throw new IllegalArgumentException("Texture unit in sampler '" + name + "' is invalid value; must be integer.");
							}
							
							program.begin();
							glUniform1i(samplerLocation, textureUnit);
							program.end();
							
							xml.next();
							xml.require(XmlPullParser.END_TAG, null, "sampler");
							
							break;
						}
						default:
							throw new IllegalArgumentException("Invalid element in program '" + id + "'.");
					}
				}
				
				xml.require(XmlPullParser.END_TAG, null, "prog");
				
				count++;
			} while(xml.nextTag() == XmlPullParser.START_TAG && xml.getName().equals("prog"));
			
			xml.require(XmlPullParser.START_TAG, null, "node");
			
			count = 0;
			do {
				String name = xml.getAttributeValue(null, "name");
				String mesh = xml.getAttributeValue(null, "mesh");
				
				throwIfNull(name, "name", "node " + count);
				throwIfNull(mesh, "mesh", "node " + count);
				
				String prog = xml.getAttributeValue(null, "prog");
				String position = xml.getAttributeValue(null, "pos");
				String orient = xml.getAttributeValue(null, "orient");
				String scale = xml.getAttributeValue(null, "scale");
				
				throwIfNull(position, "pos", "node " + count);
				
				if(nodes.containsKey(name))
					throw new IllegalArgumentException("Node named '" + name + "' already exists.");
				
				if(!meshes.containsKey(mesh))
					throw new IllegalArgumentException("Mesh named '" + mesh + "' in node '" + name + "' does not exist.");
				
				if(prog != null) {
					if(!programs.containsKey(prog))
						throw new IllegalArgumentException("Program named '" + prog + "' in node '" + name + "' does not exist.");
				}
				
				Vector3 nodePos;
				try {
					nodePos = Utils.parseVector3(position);
				} catch(Exception exc) {
					throw new IllegalArgumentException("Invalid Vector3 for position in node '" + name + "'.");
				}
				
				ArrayList<TextureBinding> texBindings = readNodeTextures(name, xml);
				
				HashMap<String,Variant> variants = new HashMap<>();
				
				int variantCount = 0;
				while(xml.getName().equals("variant")) {
					String variantName = xml.getAttributeValue(null, "name");
					String variantProg = xml.getAttributeValue(null, "prog");
					String variantBase = xml.getAttributeValue(null, "base");
					
					throwIfNull(variantName, "name", "variant " + variantCount + " in node '" + name + "'");
					if(variantProg == null && variantBase == null)
						throw new IllegalArgumentException("Variant " + variantCount + " missing base or program in node '" + name + "'.");
					if(variantProg != null && variantBase != null)
						throw new IllegalArgumentException("Variant " + variantCount + " cannot have both base and program in node '" + name + "'.");
					
					if(variants.containsKey(variantName))
						throw new IllegalArgumentException("Variant named '" + variantName + "' already exists in node '" + name + "'.");
					
					ArrayList<TextureBinding> variantTexBindings = readNodeTextures(variantName, xml);
					
					Variant variant;
					if(prog != null) {
						if(!programs.containsKey(prog))
							throw new IllegalArgumentException("Program named '" + prog + "' in variant '" + variantName + "' in node '" + name + "' does not exist.");
						
						variant = new Variant(programs.get(prog), variantTexBindings);
					}
					else
						variant = new Variant(null, variantTexBindings);
					
					variants.put(name, variant);
					
					xml.require(XmlPullParser.END_TAG, null, "variant");
					
					xml.nextTag();
				}
				
				SceneNode node = new SceneNode(meshes.get(mesh), programs.get(prog), nodePos, texBindings, variants);
				nodes.put(name, node);
				
				if(orient != null) {
					try {
						node.setOrient(Utils.parseQuaternion(orient));
					} catch(Exception exc) {
						throw new IllegalArgumentException("Invalid quaternion for orient at node '" + node + "'.", exc);
					}
				}
				
				if(scale != null) {
					try {
						node.setScale(Utils.parseVector3(scale));
					} catch(Exception exc) {
						try {
							node.setScale(new Vector3(Float.parseFloat(scale)));
						} catch(Exception exc2) {
							throw new IllegalArgumentException("Invalid Vector3 or float for scale at node '" + node + "'.");
						}
					}
				}
				
				xml.require(XmlPullParser.END_TAG, null, "node");
				
				count++;
			} while(xml.nextTag() == XmlPullParser.START_TAG && xml.getName().equals("node"));
		}
	}
	
	private ArrayList<TextureBinding> readNodeTextures(String name, XmlPullParser xml) throws IOException, XmlPullParserException {
		ArrayList<TextureBinding> texBindings = new ArrayList<>();
		ArrayList<Integer> texUnits = new ArrayList<>();
		
		while(xml.nextTag() == XmlPullParser.START_TAG && xml.getName().equals("texture")) {
			String texName = xml.getAttributeValue(null, "name");
			String unit = xml.getAttributeValue(null, "unit");
			
			throwIfNull(texName, "name", "texture in node '" + name + "'");
			throwIfNull(unit, "unit", "texture in node '" + name + "'");
			
			if(!textures.containsKey(texName))
				throw new IllegalArgumentException("Texture named '" + texName + "' in node '" + name + "' does not exist.");
			
			int texUnit;
			try {
				texUnit = Integer.parseInt(unit);
			} catch(Exception exc) {
				throw new IllegalArgumentException("Invalid value for unit at texture '" + texName + "' at node '" + name + "'.");
			}
			
			if(texUnits.contains(texUnit))
				throw new IllegalArgumentException("Texture unit used more than once at texture '" + texName + "' at node '" + name + "'.");
			
			texUnits.add(texUnit);
			
			final int[] magFilterGLEnums = {
					GL_NEAREST,
					GL_LINEAR,
					GL_NEAREST,
					GL_LINEAR
			};
			
			final int[] minFilterGLEnums = {
					GL_NEAREST,
					GL_LINEAR,
					GL_NEAREST_MIPMAP_NEAREST,
					GL_NEAREST_MIPMAP_LINEAR
			};
			
			final int[] edgeGLEnums = {
					GL_CLAMP_TO_EDGE,
					GL_CLAMP_TO_BORDER,
					GL_REPEAT,
					GL_MIRRORED_REPEAT
			};
			
			final String[] filterNames = {
					"nearest",
					"linear",
					"mipmap nearest",
					"mipmap linear"
			};
			
			final String[] edgeNames = {
					"clamp edge",
					"clamp border",
					"repeat",
					"mirror repeat"
			};
			
			String filterMode = xml.getAttributeValue(null, "sampler-filter");
			throwIfNull(filterMode, "sampler-filter", "texture in node '" + name + "'");
			
			int filter = -1;
			for(int a = 0; a < filterNames.length; a++)
				if(filterNames[a].equals(filterMode.toLowerCase())) {
					filter = a;
					break;
				}
			
			if(filter == -1)
				throw new IllegalArgumentException("Invalid sampler-filter value '" + filterMode + "' at texture in node '" + name + "'.");
			
			String edgeMode = xml.getAttributeValue(null, "sampler-edge");
			
			int edge;
			if(edgeMode == null)
				edge = 2;
			else {
				edge = -1;
				for(int a = 0; a < edgeNames.length; a++)
					if(edgeNames[a].equals(edgeMode.toLowerCase())) {
						edge = a;
						break;
					}
				
				if(edge == -1)
					throw new IllegalArgumentException("Invalid sampler-edge value '" + edgeMode + "' at texture in node '" + name + "'.");
			}
			
			String anisoMode = xml.getAttributeValue(null, "sampler-aniso");
			
			float aniso;
			if(anisoMode == null)
				aniso = 0;
			else {
				anisoMode = anisoMode.toLowerCase();
				
				float max = glGetFloat(GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT);
				
				switch(anisoMode) {
					case "none":
						aniso = 0;
						break;
					case "half":
						aniso = max / 2;
						break;
					case "max":
						aniso = max;
						break;
					default:
						try {
							aniso = Integer.parseInt(anisoMode);
							
							if(aniso > max)
								aniso = max;
						} catch(Exception exc) {
							throw new IllegalArgumentException("Invalid value for sampler-aniso '" + anisoMode + "' at texture in node '" + name + "'.");
						}
				}
			}
			
			int sampler = glGenSamplers();
			glSamplerParameteri(sampler, GL_TEXTURE_WRAP_S, edgeGLEnums[edge]);
			glSamplerParameteri(sampler, GL_TEXTURE_WRAP_T, edgeGLEnums[edge]);
			glSamplerParameteri(sampler, GL_TEXTURE_WRAP_R, edgeGLEnums[edge]);
			
			glSamplerParameteri(sampler, GL_TEXTURE_MAG_FILTER, magFilterGLEnums[filter]);
			glSamplerParameteri(sampler, GL_TEXTURE_MIN_FILTER, minFilterGLEnums[filter]);
			
			if(aniso > 0)
				glSamplerParameterf(sampler, GL_TEXTURE_MAX_ANISOTROPY_EXT, aniso);
			
			texBindings.add(new TextureBinding(textures.get(texName), texUnit, sampler));
			
			xml.next();
			xml.require(XmlPullParser.END_TAG, null, "texture");
		}
		
		return texBindings;
	}
	
	private void throwIfNull(String s, String name, String type) {
		if(s == null)
			throw new IllegalArgumentException("No " + name + " at " + type + ".");
	}
	
	public SceneNode findNode(String nodeName) {
		return nodes.get(nodeName);
	}
	
	public ShaderProgram findProgram(String programName) {
		return programs.get(programName).program;
	}
	
	public Mesh findMesh(String meshName) {
		return meshes.get(meshName);
	}
	
	public SceneTexture findTexture(String textureName) {
		return textures.get(textureName);
	}
	
	public void render(Matrix4 cameraMatrix) {
		for(SceneNode node : nodes.values())
			node.render(cameraMatrix);
	}
	
	public static class Transform {
		private Quaternion orient = new Quaternion();
		private Vector3 scale = new Vector3(1);
		private Vector3 translate = new Vector3();
		
		public Matrix4 getMatrix() {
			return new Matrix4().clearToIdentity().translate(translate).mult(orient.toMatrix()).scale(scale);
		}
	}
	
	public static class SceneTexture {
		public int texture;
		public int type;
		
		public SceneTexture(InputStream is, int creationFlags) throws IOException {
			if(is == null)
				throw new IOException("Invalid InputStream");
			
			ImageSet imageSet = DdsLoader.load(is);
			texture = TextureGenerator.createTexture(imageSet, creationFlags);
			type = TextureGenerator.getTextureType(imageSet, creationFlags);
		}
	}
	
	public static class SceneProgram {
		private ShaderProgram program;
		private int matrixUniform;
		private int normalMatrixUniform;
		private int invNormalMatrixUniform;
		
		public SceneProgram(ShaderProgram program, int matrixUniform, int normalMatrixUniform, int invNormalMatrixUniform) {
			this.program = program;
			this.matrixUniform = matrixUniform;
			this.normalMatrixUniform = normalMatrixUniform;
			this.invNormalMatrixUniform = invNormalMatrixUniform;
		}
	}
	
	public static class SceneNode {
		private Mesh mesh;
		private Variant baseVariant;
		
		private HashMap<String,Variant> variants;
		private ArrayList<StateBinder> binders;
		
		private Transform nodeTransform = new Transform();
		private Transform objectTransform = new Transform();
		
		public SceneNode(Mesh mesh, SceneProgram program, Vector3 nodePos, ArrayList<TextureBinding> texBindings, HashMap<String,Variant> variants) {
			this.mesh = mesh;
			
			baseVariant = new Variant(program, texBindings);
			
			this.variants = variants;
			
			binders = new ArrayList<>();
			
			nodeTransform.translate.set(nodePos);
		}
		
		public void setScale(Vector3 scale) {
			nodeTransform.scale.set(scale);
		}
		
		public void rotate(Quaternion orient) {
			nodeTransform.orient.mult(orient);
		}
		
		public void setOrient(Quaternion orient) {
			nodeTransform.orient.set(orient).normalize();
		}
		
		public Quaternion getOrient() {
			return nodeTransform.orient.copy();
		}
		
		public void offset(Vector3 offset) {
			nodeTransform.translate.add(offset);
		}
		
		public void setTranslate(Vector3 translate) {
			nodeTransform.translate.set(translate);
		}
		
		public void setStateBinder(StateBinder binder) {
			binders.add(binder);
		}
		
		public ShaderProgram getProgram() {
			return baseVariant.program.program;
		}
		
		public void render(Matrix4 baseMatrix) {
			if(baseVariant.program != null)
				render(baseVariant, baseMatrix);
		}
		
		public void render(String variation, Matrix4 baseMatrix) {
			Variant v = variants.get(variation);
			if(v == null)
				throw new IllegalArgumentException("Invalid variation");
			
			render(v, baseMatrix);
		}
		
		private void render(Variant variant, Matrix4 baseMatrix) {
			baseMatrix = baseMatrix.copy().mult(nodeTransform.getMatrix());
			Matrix4 objectMatrix = baseMatrix.copy().mult(objectTransform.getMatrix());
			
			variant.program.program.begin();
			glUniformMatrix4(variant.program.matrixUniform, false, objectMatrix.toBuffer());
			
			if(variant.program.normalMatrixUniform != -1)
				glUniformMatrix3(variant.program.normalMatrixUniform, false, new Matrix3(objectMatrix.inverse().transpose()).toBuffer());
			
			if(variant.program.invNormalMatrixUniform != -1)
				glUniformMatrix3(variant.program.invNormalMatrixUniform, false, new Matrix3(objectMatrix.inverse().transpose()).inverse().toBuffer());
			
			for(StateBinder binder : binders)
				binder.bindState(variant.program.program);
			
			for(TextureBinding binding : variant.texBindings) {
				glActiveTexture(GL_TEXTURE0 + binding.texUnit);
				glBindTexture(binding.tex.type, binding.tex.texture);
				glBindSampler(binding.texUnit, binding.sampler);
			}
			
			mesh.render();
			
			for(TextureBinding binding : variant.texBindings) {
				glActiveTexture(GL_TEXTURE0 + binding.texUnit);
				glBindTexture(binding.tex.type, 0);
				glBindSampler(binding.texUnit, 0);
			}
			
			for(StateBinder binder : binders)
				binder.unbindState(variant.program.program);
			
			variant.program.program.end();
		}
		
		public static class Variant {
			private SceneProgram program;
			private ArrayList<TextureBinding> texBindings;
			
			public Variant(SceneProgram program, ArrayList<TextureBinding> texBindings) {
				this.program = program;
				this.texBindings = texBindings;
			}
		}
	}
	
	public static class TextureBinding {
		private SceneTexture tex;
		private int texUnit;
		private int sampler;
		
		public TextureBinding(SceneTexture tex, int texUnit, int sampler) {
			this.tex = tex;
			this.texUnit = texUnit;
			this.sampler = sampler;
		}
	}
}
 public static class SceneNode {
	private Mesh mesh;
	private Variant baseVariant;
		
	private HashMap<String,Variant> variants;
	private ArrayList<StateBinder> binders;
		
	private Transform nodeTransform = new Transform();
	private Transform objectTransform = new Transform();
		
	public SceneNode(Mesh mesh, SceneProgram program, Vector3 nodePos, ArrayList<TextureBinding> texBindings, HashMap<String,Variant> variants) {
		this.mesh = mesh;
			
		baseVariant = new Variant(program, texBindings);
			
		this.variants = variants;
			
		binders = new ArrayList<>();
			
		nodeTransform.translate.set(nodePos);
	}
		
	public void setScale(Vector3 scale) {
		nodeTransform.scale.set(scale);
	}
		
	public void rotate(Quaternion orient) {
		nodeTransform.orient.mult(orient);
	}
		
	public void setOrient(Quaternion orient) {
		nodeTransform.orient.set(orient).normalize();
	}
		
	public Quaternion getOrient() {
		return nodeTransform.orient.copy();
	}
		
	public void offset(Vector3 offset) {
		nodeTransform.translate.add(offset);
	}
		
	public void setTranslate(Vector3 translate) {
		nodeTransform.translate.set(translate);
	}
		
	public void setStateBinder(StateBinder binder) {
		binders.add(binder);
	}
		
	public ShaderProgram getProgram() {
		return baseVariant.program.program;
	}
		
	public void render(Matrix4 baseMatrix) {
		if(baseVariant.program != null)
			render(baseVariant, baseMatrix);
	}
		
	public void render(String variation, Matrix4 baseMatrix) {
		Variant v = variants.get(variation);
		if(v == null)
			throw new IllegalArgumentException("Invalid variation");
			
		render(v, baseMatrix);
	}
		
	private void render(Variant variant, Matrix4 baseMatrix) {
		baseMatrix = baseMatrix.copy().mult(nodeTransform.getMatrix());
		Matrix4 objectMatrix = baseMatrix.copy().mult(objectTransform.getMatrix());
			
		variant.program.program.begin();
		glUniformMatrix4(variant.program.matrixUniform, false, objectMatrix.toBuffer());
			
		if(variant.program.normalMatrixUniform != -1)
			glUniformMatrix3(variant.program.normalMatrixUniform, false, new Matrix3(objectMatrix.inverse().transpose()).toBuffer());
			
		if(variant.program.invNormalMatrixUniform != -1)
			glUniformMatrix3(variant.program.invNormalMatrixUniform, false, new Matrix3(objectMatrix.inverse().transpose()).inverse().toBuffer());
			
		for(StateBinder binder : binders)
			binder.bindState(variant.program.program);
			
		for(TextureBinding binding : variant.texBindings) {
			glActiveTexture(GL_TEXTURE0 + binding.texUnit);
			glBindTexture(binding.tex.type, binding.tex.texture);
			glBindSampler(binding.texUnit, binding.sampler);
		}
			
		mesh.render();
			
		for(TextureBinding binding : variant.texBindings) {
			glActiveTexture(GL_TEXTURE0 + binding.texUnit);
			glBindTexture(binding.tex.type, 0);
			glBindSampler(binding.texUnit, 0);
		}
			
		for(StateBinder binder : binders)
			binder.unbindState(variant.program.program);
			
		variant.program.program.end();
	}
		
	public static class Variant {
		private SceneProgram program;
		private ArrayList<TextureBinding> texBindings;
			
		public Variant(SceneProgram program, ArrayList<TextureBinding> texBindings) {
			this.program = program;
			this.texBindings = texBindings;
		}
	}
}
 public class UniformIntBinder extends UniformBinderBase {
	private int value;
	
	public UniformIntBinder() {}
	
	public UniformIntBinder(int value) {
		setValue(value);
	}
	
	public void setValue(int value) {
		this.value = value;
	}
	
	public int getValue() {
		return value;
	}
	
	@Override
	public void bindState(ShaderProgram program) {
		glUniform1i(getUniformLocation(program), value);
	}
	
	@Override
	public void unbindState(ShaderProgram program) {}
}
 public class UniformMat4Binder extends UniformBinderBase {
	private Matrix4 value = new Matrix4().clearToIdentity();
	
	public UniformMat4Binder() {}
	
	public UniformMat4Binder(Matrix4 mat) {
		setValue(mat);
	}
	
	public void setValue(Matrix4 mat) {
		value.set(mat);
	}
	
	public Matrix4 getValue() {
		return value.copy();
	}
	
	@Override
	public void bindState(ShaderProgram program) {
		glUniformMatrix4(getUniformLocation(program), false, value.toBuffer());
	}
	
	@Override
	public void unbindState(ShaderProgram program) {}
}
 public class UniformVec3Binder extends UniformBinderBase {
	private Vector3 value = new Vector3();
	
	public UniformVec3Binder() {}
	
	public UniformVec3Binder(Vector3 vec) {
		setValue(vec);
	}
	
	public void setValue(Vector3 vec) {
		value.set(vec);
	}
	
	public Vector3 getValue() {
		return value.copy();
	}
	
	@Override
	public void bindState(ShaderProgram program) {
		glUniform3(getUniformLocation(program), value.toBuffer());
	}
	
	@Override
	public void unbindState(ShaderProgram program) {}
}

import static org.lwjgl.opengl.GL11.*;
import static org.lwjgl.opengl.GL12.*;
import static org.lwjgl.opengl.GL13.*;
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 static org.lwjgl.opengl.GL33.*;
import java.io.IOException;
import java.nio.FloatBuffer;
import java.util.ArrayList;
import org.lwjgl.BufferUtils;
import org.lwjgl.input.Keyboard;
import org.xmlpull.v1.XmlPullParserException;
import rosick.jglsdk.glimg.DdsLoader;
import rosick.jglsdk.glimg.ImageSet;
import rosick.jglsdk.glimg.TextureGenerator;
import com.ra4king.opengl.GLProgram;
import com.ra4king.opengl.util.Mesh;
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;
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;
import com.ra4king.opengl.util.scene.Scene;
import com.ra4king.opengl.util.scene.Scene.SceneNode;
import com.ra4king.opengl.util.scene.binders.UniformIntBinder;
import com.ra4king.opengl.util.scene.binders.UniformMat4Binder;
import com.ra4king.opengl.util.scene.binders.UniformVec3Binder;

package com.ra4king.opengl.arcsynthesis.gl33.chapter17.example2;






/**
 * @author ra4king
 */
public class Example17_2 extends GLProgram {
	public static void main(String[] args) {
		new Example17_2().run(true);
	}
	
	private Scene scene;

	private ArrayList<SceneNode> nodes;
