/include/fg2/fg2.h (06910f03df775258a6951af6c642bfacee99d4e7) (77760 bytes) (mode 100755) (type blob)

#ifndef FG2_H
#define FG2_H

#if defined(__EMSCRIPTEN__)
#  define FG2_GET_CANVAS_WIDTH EM_ASM_INT( { return Module.canvas.width; }, nullptr )
#  define FG2_GET_CANVAS_HEIGHT EM_ASM_INT( { return Module.canvas.height; }, nullptr )
#  define GLAD_GLES2_IMPLEMENTATION
#  include "glad_gles2.h"
#  include <emscripten/emscripten.h>
#  include <emscripten/html5.h>
#elif defined(__ANDROID__)
#  define GLAD_GLES2_IMPLEMENTATION
#  include "glad_gles2.h"
#elif defined(__APPLE__)
#  include <TargetConditionals.h>
#  if defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE==1
#    define GLAD_GLES2_IMPLEMENTATION
#    include "glad_gles2.h"
#  else
#    define GLAD_GL_IMPLEMENTATION
#    include "glad_gl.h"
#  endif
#else
#  define GLAD_GL_IMPLEMENTATION
#  include "glad_gl.h"
#endif

#include "linalg.h"

#include <SDL2/SDL.h>

#include <stdio.h>

#include <cmath>
#include <cstring>

#include <memory>
#include <string>
#include <vector>

namespace fg2
{

bool verbose = true;

#ifdef GLAD_GL
const char* shaderHeader = R"(
#version 110
)";
#else // GLES
const char* shaderHeader = R"(
#version 100
precision mediump float;
)";
#endif

// vec2/3 can become vec4 automatically
// https://stackoverflow.com/questions/18935203/shader-position-vec4-or-vec3

const char* unlitVert = R"(
uniform mat4 u_matrices[6]; // 0:mvp 1:mv 2:m 3:normal 4:texture, 5:light
attribute vec4 a_Position;
attribute vec4 a_Normal;
attribute vec4 a_Tangent;
attribute vec4 a_UV;
varying vec2 v_UV;
varying vec4 v_RelativePos;
void main(){
	v_UV = vec2( u_matrices[4] * a_UV );
	v_RelativePos = u_matrices[1] * a_Position;
	gl_Position = u_matrices[0] * a_Position;
}
)";

const char* unlitFrag = R"(
uniform sampler2D u_texture;
uniform vec4 u_fog;
uniform vec3 u_camera;
varying vec2 v_UV;
varying vec4 v_RelativePos;
void main(){
	vec4 texColor = texture2D( u_texture, v_UV );
	if( texColor.a < 0.001 ) discard;
	if( u_fog.a > 0.0 ){
		float fogFactor = 1.0 - clamp( 1.0 / exp( length( v_RelativePos ) * u_fog.a ), 0.0, 1.0 );
		gl_FragColor = vec4( mix( texColor.rgb, u_fog.rgb, fogFactor ), texColor.a );
	}else{
		gl_FragColor = texColor;
	}
}
)";

std::vector<std::string> unlitSamplers = { "u_texture" };

const char* colorModFrag = R"(
uniform sampler2D u_texture;
uniform vec4 u_fog;
uniform vec3 u_camera;
varying vec2 v_UV;
varying vec4 v_RelativePos;
void main(){
	vec4 texColor = texture2D( u_texture, v_UV );
	if( texColor.a < 0.001 ) discard;
	gl_FragColor = texColor * u_fog;
}
)";

std::vector<std::string> colorModSamplers = { "u_texture" };

const char* skyboxVert = R"(
uniform mat4 u_matrices[6]; // 0:mvp 1:mv 2:m 3:normal 4:texture, 5:light
attribute vec4 a_Position;
attribute vec4 a_Normal;
attribute vec4 a_Tangent;
attribute vec4 a_UV;
varying vec3 v_STR;
void main(){
	v_STR = a_Position.xyz;
	vec4 pos = u_matrices[0] * a_Position;
	gl_Position = pos.xyww;
}
)";

const char* skyboxFrag = R"(
uniform samplerCube u_cubemap;
uniform vec4 u_fog;
uniform vec3 u_camera;
varying vec3 v_STR;
void main(){
	gl_FragColor = textureCube( u_cubemap, v_STR ) * u_fog;
}
)";

std::vector<std::string> skyboxSamplers = { "u_cubemap" };

const GLchar* irradianceFrag = R"(
uniform samplerCube u_cubemap;
uniform vec4 u_fog;
uniform vec3 u_camera;
varying vec3 v_STR;

const float PI = 3.1415927;

mat4 rotationMatrix( vec3 axis, float angle ){
	axis = normalize( axis );
	float s = sin( angle );
	float c = cos( angle );
	float oc = 1.0 - c;
	return mat4(
		oc * axis.x * axis.x + c,          oc * axis.x * axis.y - axis.z * s, oc * axis.z * axis.x + axis.y * s, 0.0,
		oc * axis.x * axis.y + axis.z * s, oc * axis.y * axis.y + c,          oc * axis.y * axis.z - axis.x * s, 0.0,
		oc * axis.z * axis.x - axis.y * s, oc * axis.y * axis.z + axis.x * s, oc * axis.z * axis.z + c,          0.0,
		0.0,                               0.0,                               0.0,                               1.0
	);
}

vec3 getIrradiance( vec3 normal ){
	const float grain = 0.05;
	vec3 irradiance = vec3( 0.0 );
	vec3 up = vec3( 0.0, 1.0, 0.0 );
	vec3 right = normalize( cross( normal, up ) );
	float index = 0.0;
	for( float longi = 0.0; longi <= PI * 0.5; longi += grain ){
		mat4 trl = rotationMatrix( right, longi );
		for( float azi = 0.0; azi <= PI * 2.0; azi += grain ){
			mat4 tra = rotationMatrix( normal, azi );
			vec3 sampleVec = ( tra * trl * vec4( normal, 1.0 ) ).xyz;
			irradiance += textureCube( u_cubemap, sampleVec ).rgb * sin( longi ) * cos( longi );
			index += 1.0;
		}
	}
	float hemispherePDF = 1.0 / ( 2.0 * PI );
	irradiance /= index * hemispherePDF;
	return irradiance;
}

void main(){
	vec3 N = normalize( v_STR );
	gl_FragColor = vec4( getIrradiance( N ) / PI, 1.0 );
}
)";

std::vector<std::string> irradianceSamplers = { "u_cubemap" };

struct Display {
	bool success;
	unsigned int width;
	unsigned int height;
	std::string title;
};

Display newDisplay = { false, 0, 0, "" };

struct Color {
	GLfloat r;
	GLfloat g;
	GLfloat b;
	GLfloat a;
};

Color newColor = { 1.0f, 1.0f, 1.0f, 1.0f };

Color fogColor = { 0.0f, 0.0f, 0.0f, 0.0f };

struct Texture {
	bool success;
	GLuint texture;
	GLsizei width;
	GLsizei height;
	unsigned int channels;
	bool mipmap;
	GLenum type;
};

Texture newTexture = { false, 0, 0, 0, 0, false, 0 };

Texture blankTexture = { false, 0, 0, 0, 0, false, 0 };

struct Framebuffer {
	bool success;
	GLuint fbo;
	GLuint texture;
	GLuint texture_z;
	GLenum texture_type;
	GLuint rbo;
	GLsizei width;
	GLsizei height;
};

Framebuffer newFramebuffer = { false, 0, 0, 0, 0, 0, 0, 0 };

struct Vertex {
	GLfloat Position[3];
	GLfloat Normal[3];
	GLfloat Tangent[3];
	GLfloat UV[2];
};

Vertex newVertex = {
	{ 0.0f, 0.0f, 0.0f },
	{ 0.0f, 0.0f, 0.0f },
	{ 0.0f, 0.0f, 0.0f },
	{ 0.0f, 0.0f }
};

typedef GLuint Index;

struct Pipeline {
	bool success;
	GLuint programObject;
	GLuint u_metallicFactor;
	GLuint u_roughnessFactor;
	GLuint u_baseColorFactor;
	GLuint u_emissiveFactor;
	GLuint u_matrices;
	GLuint u_fog;
	GLuint u_camera;
	std::vector<GLuint> slots;
};

Pipeline
	newPipeline = { false, 0, 0, 0, 0, 0, 0, 0, 0, {} },
	drawPipeline = { false, 0, 0, 0, 0, 0, 0, 0, 0, {} },
	unlitPipeline = { false, 0, 0, 0, 0, 0, 0, 0, 0, {} },
	colorModPipeline = { false, 0, 0, 0, 0, 0, 0, 0, 0, {} },
	unlitInstancePipeline = { false, 0, 0, 0, 0, 0, 0, 0, 0, {} },
	skyboxPipeline = { false, 0, 0, 0, 0, 0, 0, 0, 0, {} },
	irradiancePipeline = { false, 0, 0, 0, 0, 0, 0, 0, 0, {} };

struct Mesh {
	bool success;
	std::vector<Vertex> vertices;
	std::vector<Index> indices;
	GLuint buffers[2]; // vertex buffer, index buffer
	GLfloat xmin;
	GLfloat xmax;
	GLfloat ymin;
	GLfloat ymax;
	GLfloat zmin;
	GLfloat zmax;
};

Mesh newMesh = { false, {}, {}, { 0, 0 }, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };

Mesh planeMesh = { false, {}, {}, { 0, 0 }, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };

Mesh cubeMesh = { false, {}, {}, { 0, 0 }, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };

struct Font {
	Texture texture;
	Mesh textMesh;
	float size;
	float height;
	std::vector<int> charStarts;
	std::vector<int> charEnds;
	std::vector<unsigned char> buffer;
	std::vector<unsigned char> atlas;
	bool needSync;
#ifdef __STB_INCLUDE_STB_TRUETYPE_H__
	stbtt_pack_context pc;
	stbtt_fontinfo info;
	std::vector<stbtt_packedchar> packedChars;
#else
	void* pc;
	void* info;
	std::vector<void*> packedChars;
#endif
};

Font newFont = { newTexture, newMesh, 0.0f, 0.0f, {}, {}, {}, {}, false, {}, {}, {} };

// Globals.
linalg::mat<double,4,4> texMatrix = linalg::identity, lightMatrix = linalg::identity;

int mouseX = 0, mouseY = 0, mouseMoveX = 0, mouseMoveY = 0, mouseWheel = 0;
bool mouseTrapped = false;

float touchPressure = 0.0f;
bool touchStart = false;

bool hasFocus = true;

SDL_Window* window = nullptr;
SDL_GLContext ctx = 0;
SDL_GameController* controller = nullptr;
SDL_Haptic* haptic = nullptr;

SDL_Rect textInputRect = {};
bool textInputEnabled = false;
bool textReturnStart = false;
std::string textInputString = "";

Uint32 windowID = 0;
const Uint8* keystates = nullptr;
Uint64 now = 0;

std::u32string utf8ToUtf32( std::string in ){
	std::u32string out;

	for( size_t i = 0; i < in.length(); i++ ){
		// An unsigned char pointer starting at the current char.
		auto u = reinterpret_cast<unsigned char*>( &in[ i ] );

		if( u[0] < 128 ){
			// 7 bits (ASCII)
			out += u[0];
		}else if( i + 1 < in.length() ){
			if( u[0] >= 192 && u[0] < 224 ){
				// 2 bytes
				out +=
					( u[0] - 192 ) * 64 +
					( u[1] - 128 );
				i += 1;
			}else if( i + 2 < in.length() ){
				if( u[0] >= 224 && u[0] < 240 ){
					// 3 bytes
					out +=
						( u[0] - 224 ) * 4096 +
						( u[1] - 128 ) * 64 +
						( u[2] - 128 );
					i += 2;
				}else if( i + 3 < in.length() && u[0] >= 240 && u[0] < 248 ){
					// 4 bytes
					out +=
						( u[0] - 240 ) * 262144 +
						( u[1] - 128 ) * 4096 +
						( u[2] - 128 ) * 64 +
						( u[3] - 128 );
					i += 3;
				}
			}
		}
	}

	return out;
}

linalg::vec<double,4> eulerToQuat( double aX, double aY, double aZ ){
	double
		c1 = std::cos( aY * 0.5 ),
		c2 = std::cos( aZ * 0.5 ),
		c3 = std::cos( aX * 0.5 ),
		s1 = std::sin( aY * 0.5 ),
		s2 = std::sin( aZ * 0.5 ),
		s3 = std::sin( aX * 0.5 );
	
	return linalg::vec<double,4>(
		s1 * s2 * c3 + c1 * c2 * s3,
		s1 * c2 * c3 + c1 * s2 * s3,
		c1 * s2 * c3 - s1 * c2 * s3,
		c1 * c2 * c3 - s1 * s2 * s3
	);
}

linalg::vec<double,4> directionToQuat( linalg::vec<double,3> forward, linalg::vec<double,3> up ){
	forward = linalg::normalize( forward );
	up = linalg::normalize( up );
	linalg::mat<double,4,4> m = linalg::lookat_matrix( linalg::vec<double,3>(), forward, up );
	return linalg::qconj( linalg::rotation_quat( linalg::mat<double,3,3>(
		{ m[0][0], m[1][0], m[2][0] },
		{ m[0][1], m[1][1], m[2][1] },
		{ m[0][2], m[1][2], m[2][2] }
	) ) );
}

double wrapAngle( double a ){
	return std::remainder( a, std::acos( -1 ) * 2 );
}

Color rgb( float r, float g, float b ){
	Color col = { r, g, b, 1.0f };
	return col;
}

// Used by cls.
void drawMesh( Mesh &mesh, linalg::mat<double,4,4> modelMat, linalg::mat<double,4,4> viewMat, linalg::mat<double,4,4> projMat );
void setPipeline( Pipeline pipeline );

void cls( Color col, bool clearDepth = true ){
	if( clearDepth ){
		// GL_DEPTH_BUFFER_BIT is not usually problematic.
		glClear( GL_DEPTH_BUFFER_BIT );
	}
	// Clear the screen by drawing a plane rather than glClear.
	// Graphics driver bugs occasionally result in glClear-related crashes.
	Pipeline p = drawPipeline;
	setPipeline( colorModPipeline );
	glUniform4f( drawPipeline.u_fog, col.r, col.g, col.b, col.a );
	glDisable( GL_BLEND );
	glActiveTexture( GL_TEXTURE0 );
	glBindTexture( GL_TEXTURE_2D, blankTexture.texture );
	// Stretch to screen bounds at max depth.
	drawMesh(
		planeMesh,
		linalg::mat<double,4,4>(
			{ 2.0, 0.0, 0.0, 0.0 },
			{ 0.0, 2.0, 0.0, 0.0 },
			{ 0.0, 0.0, 0.0, 0.0 },
			{ 0.0, 0.0, 1.0, 1.0 }
		),
		linalg::identity,
		linalg::identity
	);
	glUniform4f( drawPipeline.u_fog, fogColor.r, fogColor.g, fogColor.b, fogColor.a );
	setPipeline( p );
}

Mesh loadMesh( std::vector<Vertex> &vertices, std::vector<Index> &indices, bool streaming = false ){
	Mesh mesh = newMesh;

	if( vertices.size() > 0 ){
		// Measure the farthest bounds of the vertices.
		// Start with the first vertex so that the origin does not matter.
		GLfloat* p = vertices[0].Position;
		mesh.xmin = p[0];
		mesh.xmax = p[0];
		mesh.ymin = p[1];
		mesh.ymax = p[1];
		mesh.zmin = p[2];
		mesh.zmax = p[2];
		for( size_t i = 1; i < vertices.size(); i++ ){
			p = vertices[i].Position;
			if( p[0] < mesh.xmin ) mesh.xmin = p[0];
			if( p[0] > mesh.xmax ) mesh.xmax = p[0];
			if( p[1] < mesh.ymin ) mesh.ymin = p[1];
			if( p[1] > mesh.ymax ) mesh.ymax = p[1];
			if( p[2] < mesh.zmin ) mesh.zmin = p[2];
			if( p[2] > mesh.zmax ) mesh.zmax = p[2];
		}
	}
	
	if( verbose )
		printf( "%s%s\n", ( streaming ? "STREAMING " : "" ), "MESH" );
	
	// Generate a vertex buffer and an index buffer.
	glGenBuffers( 2, &mesh.buffers[0] );
	if( verbose ){
		printf( "Vertex buffer address: %d\n", mesh.buffers[0] );
		printf( "Index buffer address:  %d\n", mesh.buffers[1] );
	}
	
	// Switch to the vertex buffer.
	glBindBuffer( GL_ARRAY_BUFFER, mesh.buffers[0] );
	// Upload the vertices, passing the size in bytes.
	glBufferData( GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], streaming ? GL_STREAM_DRAW : GL_STATIC_DRAW );
	if( verbose ){
		printf( "Number of vertices: %lu\n", vertices.size() );
		printf( "Size of Vertex:     %lu\n", sizeof(Vertex) );
	}
	
	// Switch to the index buffer.
	glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh.buffers[1] );
	// Upload the indices, passing the size in bytes.
	glBufferData( GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(Index), &indices[0], streaming ? GL_STREAM_DRAW : GL_STATIC_DRAW );
	if( verbose ){
		printf( "Number of indices: %lu\n", indices.size() );
		printf( "Size of Index:     %lu\n\n", sizeof(Index) );
	}
	
	// TODO: Failure conditions.
	mesh.success = true;
	
	mesh.vertices = vertices;
	mesh.indices = indices;
	
	return mesh;
}

void updateMesh( Mesh &mesh, std::vector<Vertex> &vertices, std::vector<Index> &indices, bool streaming = false ){
	// Switch to the vertex buffer.
	glBindBuffer( GL_ARRAY_BUFFER, mesh.buffers[0] );
	// Upload the vertices, passing the size in bytes.
	glBufferData( GL_ARRAY_BUFFER, vertices.size() * sizeof(Vertex), &vertices[0], streaming ? GL_STREAM_DRAW : GL_STATIC_DRAW );
	
	// Switch to the index buffer.
	glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh.buffers[1] );
	// Upload the indices, passing the size in bytes.
	glBufferData( GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(Index), &indices[0], streaming ? GL_STREAM_DRAW : GL_STATIC_DRAW );

	mesh.vertices = vertices;
	mesh.indices = indices;
}

// Get the transforms ready and upload them to the graphics API.
void uploadModelTransforms( const linalg::mat<double,4,4> &modelMat, const linalg::mat<double,4,4> &viewMat, const linalg::mat<double,4,4> &projMat ){
	linalg::mat<double,4,4> mv = linalg::mul( linalg::inverse( viewMat ), modelMat );
	linalg::mat<double,4,4> mvp = linalg::mul( projMat, mv );
	linalg::mat<double,4,4> normal = linalg::transpose( linalg::inverse( modelMat ) );
	
	const GLfloat glmats[96] = { // TODO: Don't send all matrices at once.
		(GLfloat)mvp[0][0], (GLfloat)mvp[0][1], (GLfloat)mvp[0][2], (GLfloat)mvp[0][3],
		(GLfloat)mvp[1][0], (GLfloat)mvp[1][1], (GLfloat)mvp[1][2], (GLfloat)mvp[1][3],
		(GLfloat)mvp[2][0], (GLfloat)mvp[2][1], (GLfloat)mvp[2][2], (GLfloat)mvp[2][3],
		(GLfloat)mvp[3][0], (GLfloat)mvp[3][1], (GLfloat)mvp[3][2], (GLfloat)mvp[3][3],
		(GLfloat)mv[0][0], (GLfloat)mv[0][1], (GLfloat)mv[0][2], (GLfloat)mv[0][3],
		(GLfloat)mv[1][0], (GLfloat)mv[1][1], (GLfloat)mv[1][2], (GLfloat)mv[1][3],
		(GLfloat)mv[2][0], (GLfloat)mv[2][1], (GLfloat)mv[2][2], (GLfloat)mv[2][3],
		(GLfloat)mv[3][0], (GLfloat)mv[3][1], (GLfloat)mv[3][2], (GLfloat)mv[3][3],
		(GLfloat)modelMat[0][0], (GLfloat)modelMat[0][1], (GLfloat)modelMat[0][2], (GLfloat)modelMat[0][3],
		(GLfloat)modelMat[1][0], (GLfloat)modelMat[1][1], (GLfloat)modelMat[1][2], (GLfloat)modelMat[1][3],
		(GLfloat)modelMat[2][0], (GLfloat)modelMat[2][1], (GLfloat)modelMat[2][2], (GLfloat)modelMat[2][3],
		(GLfloat)modelMat[3][0], (GLfloat)modelMat[3][1], (GLfloat)modelMat[3][2], (GLfloat)modelMat[3][3],
		(GLfloat)normal[0][0], (GLfloat)normal[0][1], (GLfloat)normal[0][2], (GLfloat)normal[0][3],
		(GLfloat)normal[1][0], (GLfloat)normal[1][1], (GLfloat)normal[1][2], (GLfloat)normal[1][3],
		(GLfloat)normal[2][0], (GLfloat)normal[2][1], (GLfloat)normal[2][2], (GLfloat)normal[2][3],
		(GLfloat)normal[3][0], (GLfloat)normal[3][1], (GLfloat)normal[3][2], (GLfloat)normal[3][3],
		(GLfloat)texMatrix[0][0], (GLfloat)texMatrix[0][1], (GLfloat)texMatrix[0][2], (GLfloat)texMatrix[0][3],
		(GLfloat)texMatrix[1][0], (GLfloat)texMatrix[1][1], (GLfloat)texMatrix[1][2], (GLfloat)texMatrix[1][3],
		(GLfloat)texMatrix[2][0], (GLfloat)texMatrix[2][1], (GLfloat)texMatrix[2][2], (GLfloat)texMatrix[2][3],
		(GLfloat)texMatrix[3][0], (GLfloat)texMatrix[3][1], (GLfloat)texMatrix[3][2], (GLfloat)texMatrix[3][3],
		(GLfloat)lightMatrix[0][0], (GLfloat)lightMatrix[0][1], (GLfloat)lightMatrix[0][2], (GLfloat)lightMatrix[0][3],
		(GLfloat)lightMatrix[1][0], (GLfloat)lightMatrix[1][1], (GLfloat)lightMatrix[1][2], (GLfloat)lightMatrix[1][3],
		(GLfloat)lightMatrix[2][0], (GLfloat)lightMatrix[2][1], (GLfloat)lightMatrix[2][2], (GLfloat)lightMatrix[2][3],
		(GLfloat)lightMatrix[3][0], (GLfloat)lightMatrix[3][1], (GLfloat)lightMatrix[3][2], (GLfloat)lightMatrix[3][3],
	};
	
	glUniformMatrix4fv( drawPipeline.u_matrices, 6, GL_FALSE, glmats );
	
	// Send the camera position relative to the object's origin.
	glUniform3f(
		drawPipeline.u_camera,
		viewMat[3][0] - modelMat[3][0],
		viewMat[3][1] - modelMat[3][1],
		viewMat[3][2] - modelMat[3][2]
	);
}

void drawMesh( Mesh &mesh, linalg::mat<double,4,4> modelMat, linalg::mat<double,4,4> viewMat, linalg::mat<double,4,4> projMat ){
	if( !mesh.success ) return;
	
	uploadModelTransforms( modelMat, viewMat, projMat );
	
	glBindBuffer( GL_ARRAY_BUFFER, mesh.buffers[0] );
	glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mesh.buffers[1] );
	
	// turn on 4 attribute arrays
	glEnableVertexAttribArray( 0 ); // Position
	glEnableVertexAttribArray( 1 ); // Normal
	glEnableVertexAttribArray( 2 ); // Tangent
	glEnableVertexAttribArray( 3 ); // UV
	
	size_t pointer = 0;
	glVertexAttribPointer( 0, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)pointer ); // Position
	pointer += sizeof(newVertex.Position);
	glVertexAttribPointer( 1, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)pointer ); // Normal
	pointer += sizeof(newVertex.Normal);
	glVertexAttribPointer( 2, 3, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)pointer ); // Tangent
	pointer += sizeof(newVertex.Tangent);
	glVertexAttribPointer( 3, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (const GLvoid*)pointer ); // UV
	
	glDrawElements( GL_TRIANGLES, mesh.indices.size(), GL_UNSIGNED_INT, (const GLvoid*)0 );
}

void normalizeVertices( std::vector<Vertex> &verts ){
	for( size_t i = 0; i < verts.size(); i++ ){
		GLfloat* n = verts[i].Normal;
		GLfloat l = std::sqrt( n[0] * n[0] + n[1] * n[1] + n[2] * n[2] );
		if( l != 0.0f && l != 1.0f ){
			verts[i].Normal[0] /= l;
			verts[i].Normal[1] /= l;
			verts[i].Normal[2] /= l;
		}
	}
}

Mesh loadOBJ( std::string filepath ){
	if( verbose ) printf( "OBJ MESH\n" );
	size_t line = 0;

	#ifndef __EMSCRIPTEN__
	try{
	#endif

		FILE* file = fopen( filepath.c_str(), "rb" );
		
		if( !file ){
			fprintf( stderr, "Failed to open %s\n\n", filepath.c_str() );
			return newMesh;
		}
		
		std::string text = "";
		char buf[4096];
		while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
			text += std::string( buf, len );
		}
		fclose( file );
		
		struct float3 { GLfloat x, y, z; };
		struct float2 { GLfloat u, v; };
		struct obdex { int v1, vt1, vn1,  v2, vt2, vn2,  v3, vt3, vn3; };
		
		std::vector<float3> obj_v, obj_vn;
		std::vector<float2> obj_vt;
		std::vector<obdex> obj_f;
		
		size_t head = 0, tail = 0;
		
		do{
			line++;
			tail = text.find_first_of( "\n", head );
			if( tail == std::string::npos ) tail = text.size();
			if( text[ head ] != '#' ){
				std::string ln = text.substr( head, tail - head );
				
				std::vector<std::string> s;
				
				size_t i1 = 0, i2 = 0;
				
				do{
					i2 = ln.find_first_of( " \r/", i1 );
					if( i2 == std::string::npos ) i2 = ln.size();
					std::string value = ln.substr( i1, i2 - i1 );
					if( value.length() > 0 ) s.push_back( value );
					i1 = i2 + 1;
				}while( i1 < ln.size() );
				
				if( s.size() > 0 ){
					if( s[0] == "v" && s.size() >= 4 ){
						obj_v.push_back( { std::stof(s[1]), std::stof(s[2]), std::stof(s[3]) } );
					}else if( s[0] == "vt" && s.size() >= 3 ){
						// Flip the vertical texture coordinate.
						obj_vt.push_back( { std::stof(s[1]), 1.0f - std::stof(s[2]) } );
					}else if( s[0] == "vn" && s.size() >= 4 ){
						obj_vn.push_back( { std::stof(s[1]), std::stof(s[2]), std::stof(s[3]) } );
					}else if( s[0] == "f" && s.size() >= 10 ){
						obj_f.push_back( {
							std::stoi(s[1]),
							std::stoi(s[2]),
							std::stoi(s[3]),
							std::stoi(s[4]),
							std::stoi(s[5]),
							std::stoi(s[6]),
							std::stoi(s[7]),
							std::stoi(s[8]),
							std::stoi(s[9])
						} );
					}
				}
			}
			head = tail + 1;
		}while( head < text.size() );
		
		if( verbose ){
			printf( "Vertex positions: %lu\n", obj_v.size() );
			printf( "Vertex texcoords: %lu\n", obj_vt.size() );
			printf( "Vertex normals:   %lu\n", obj_vn.size() );
			printf( "Faces: %lu\n", obj_f.size() );
		}
		
		std::vector<Vertex> verts;
		std::vector<Index> inds;
		
		// Create a vertex for each index. Inefficient but effective.
		// https://community.khronos.org/t/obj-texture-coordinates-arent-mapped-correctly/69926
		Index idx = 0;
		for( obdex f : obj_f ){
			const int lookup_v[] = { f.v1, f.v2, f.v3 };
			const int lookup_vt[] = { f.vt1, f.vt2, f.vt3 };
			const int lookup_vn[] = { f.vn1, f.vn2, f.vn3 };
			for( int i = 0; i < 3; i++ ){
				int v = lookup_v[i] - 1;
				int vt = lookup_vt[i] - 1;
				int vn = lookup_vn[i] - 1;
				verts.push_back( {
					{ obj_v[v].x, obj_v[v].y, obj_v[v].z },
					{ obj_vn[vn].x, obj_vn[vn].y, obj_vn[vn].z },
					{ 0.0f, 0.0f, 0.0f },
					{ obj_vt[vt].u, obj_vt[vt].v }
				} );
				inds.push_back( idx );
				idx++;
			}
		}
		
		normalizeVertices( verts );
		
		if( verbose ){
			printf( "Vertices: %lu\n", verts.size() );
			printf( "Indices: %lu\n\n", inds.size() );
		}
		
		return loadMesh( verts, inds );

	#ifndef __EMSCRIPTEN__
	}catch( const std::exception &e ){
		fprintf( stderr, "Caught exception at line %lu of %s: %s\n\n", line, filepath.c_str(), e.what() );
	}
	#endif

	return newMesh;
}

#ifdef tinyply_h

Mesh loadPLY( std::string filepath ){
	if( verbose ) printf( "PLY MESH\n" );

	#ifndef __EMSCRIPTEN__
	try{
	#endif

		std::ifstream ss( filepath, std::ios::binary );
		
		if( ss.fail() ){
			fprintf( stderr, "Failed to open %s\n\n", filepath.c_str() );
			return newMesh;
		}
		
		PlyFile file;
		file.parse_header( ss );
		
		if( verbose ){
			for( auto c : file.get_comments() ){
				std::cout << "Comment: " << c << std::endl;
			}
		}
		
		// Tinyply treats parsed data as untyped byte buffers.
		std::shared_ptr<PlyData> vertices, normals, texcoords, faces;
		
		bool gotNormals = false, gotUV = false;
		
		for( auto e : file.get_elements() ){
			if( verbose )
				std::cout << "element - " << e.name << " (" << e.size << ")" << std::endl;
			for( auto p : e.properties ){
				if( verbose )
					std::cout << "\tproperty - " << p.name << " (" << tinyply::PropertyTable[p.propertyType].str << ")" << std::endl;
				
				if( e.name == "vertex" ){
					if( p.name == "x" ){
						// assume if "x" exists then so do "y" and "z"
						vertices = file.request_properties_from_element( e.name, { "x", "y", "z" } );
					}else if( p.name == "nx" ){
						// assume if "nx" exists then so do "ny" and "nz"
						normals = file.request_properties_from_element( e.name, { "nx", "ny", "nz" } );
						gotNormals = true;
					}else if( p.name == "u" ){
						// assume if "u" exists then so does "v"
						texcoords = file.request_properties_from_element( e.name, { "u", "v" } );
						gotUV = true;
					}else if( p.name == "s" ){
						// assume if "s" exists then so does "t"
						texcoords = file.request_properties_from_element( e.name, { "s", "t" } );
						gotUV = true;
					}
				}else if( e.name == "face" && p.name == "vertex_indices" ){
					faces = file.request_properties_from_element( e.name, { p.name }, 3 );
				}
			}
		}
		
		file.read( ss );
		
		if( verbose ){
			if( vertices )
				std::cout << "Read " << vertices->count << " total vertices." << std::endl;
			if( normals )
				std::cout << "Read " << normals->count << " total vertex normals." << std::endl;
			if( texcoords )
				std::cout << "Read " << texcoords->count << " total vertex texcoords." << std::endl;
			if( faces )
				std::cout << "Read " << faces->count << " total faces (triangles).\n" << std::endl;
		}
		
		if( vertices->t == tinyply::Type::FLOAT32 && ( faces->t == tinyply::Type::INT32 || faces->t == tinyply::Type::UINT32 ) ){
			struct float3 { GLfloat x, y, z; };
			struct float2 { GLfloat u, v; };
			
			// vertex positions
			std::vector<float3> vertpos( vertices->count );
			const size_t numVerticesBytes = vertices->buffer.size_bytes();
			std::memcpy( vertpos.data(), vertices->buffer.get(), numVerticesBytes );
			
			// vertex normals
			std::vector<float3> vertnorms( gotNormals ? normals->count : 1 );
			if( gotNormals ){
				const size_t numNormalsBytes = normals->buffer.size_bytes();
				std::memcpy( vertnorms.data(), normals->buffer.get(), numNormalsBytes );
			}
			
			// vertex UV
			std::vector<float2> vertuv( gotUV ? texcoords->count : 1 );
			if( gotUV ){
				const size_t numTexcoordsBytes = texcoords->buffer.size_bytes();
				std::memcpy( vertuv.data(), texcoords->buffer.get(), numTexcoordsBytes );
			}
			
			// create the vertex array
			std::vector<Vertex> verts;
			
			float3 n = { 0.0f, 0.0f, 0.0f };
			float2 uv = { 0.0f, 0.0f };
			
			// fill vertex array with vertex attributes from tinyply
			for( size_t i = 0; i < vertpos.size(); i++ ){
				if( gotNormals ) n = { vertnorms[i].x, vertnorms[i].y, vertnorms[i].z };
				if( gotUV ) uv = { vertuv[i].u, vertuv[i].v };
				Vertex v = {
					{ vertpos[i].x, vertpos[i].y, vertpos[i].z },
					{ n.x, n.y, n.z },
					{ 0.0f, 0.0f, 0.0f },
					// Flip the vertical texture coordinate.
					{ uv.u, 1.0f - uv.v },
				};
				verts.push_back( v );
			}
			
			// normalize the vertex array
			normalizeVertices( verts );
			
			std::vector<int32_t> indpos( faces->count * 3 );
			const size_t numFacesBytes = faces->buffer.size_bytes();
			std::memcpy( indpos.data(), faces->buffer.get(), numFacesBytes );
			
			// detect if signed int32 is used for indices
			bool sint = faces->t == tinyply::Type::INT32;
			
			// create the index array
			std::vector<Index> inds;
			
			// fill index array with indices from tinyply
			for( int32_t i : indpos ) inds.push_back( sint ? (Index)i : (Index)*reinterpret_cast<uint32_t*>(&i) );
			
			return loadMesh( verts, inds );
		}else{
			fprintf( stderr, "PLY mesh does not use 32-bit format\n\n" );
		}

	#ifndef __EMSCRIPTEN__
	}catch( const std::exception &e ){
		fprintf( stderr, "Caught exception with %s: %s\n\n", filepath.c_str(), e.what().c_str() );
	}
	#endif

	return newMesh;
}

#else

Mesh loadPLY( std::string filepath ){
	(void)filepath;
	fprintf( stderr, "Include tinyply.h before fg2.h to load PLY files.\n\n" );
	return newMesh;
}

#endif // tinyply_h

void freeMesh( Mesh &mesh ){
	glDeleteBuffers( 2, mesh.buffers );
	mesh = newMesh;
}

Texture loadTexture( const GLvoid* data, GLsizei width, GLsizei height, unsigned int channels, bool mipmap = true, bool filter = true ){
	Texture tex = newTexture;
	if( !data ) return tex;
	
	// GL_LUMINANCE and GL_LUMINANCE_ALPHA do not exist in GLES3 and later.
	GLenum pixfmts[] = {
		GL_LUMINANCE,
		GL_LUMINANCE_ALPHA,
		GL_RGB,
		GL_RGBA
	};
	
	glGenTextures( 1, &tex.texture );
	glBindTexture( GL_TEXTURE_2D, tex.texture );
	
	bool canRepeat = true;
	#ifdef __EMSCRIPTEN__
		// https://stackoverflow.com/questions/600293/how-to-check-if-a-number-is-a-power-of-2
		if( ( width & ( width - 1 ) ) != 0 || ( height & ( height - 1 ) ) != 0 ){
			canRepeat = false;
			mipmap = false;
		}
	#endif
	
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, canRepeat ? GL_REPEAT : GL_CLAMP_TO_EDGE );
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, canRepeat ? GL_REPEAT : GL_CLAMP_TO_EDGE );
	
	glTexParameteri(
		GL_TEXTURE_2D,
		GL_TEXTURE_MIN_FILTER,
		mipmap ? ( filter ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_LINEAR ) : ( filter ? GL_LINEAR : GL_NEAREST )
	);
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, filter ? GL_LINEAR : GL_NEAREST );
	
	// STB libraries do not pad rows
	// https://www.opengl.org/discussion_boards/showthread.php/134151-glTexImage2D-Resulting-texture-not-matching-passed-data
	// https://stackoverflow.com/questions/15052463/given-the-pitch-of-an-image-how-to-calculate-the-gl-unpack-alignment
	unsigned int pitch = width * channels;
	glPixelStorei(
		GL_UNPACK_ALIGNMENT,
		    pitch % 8 == 0 ? 8 // most efficient
		: ( pitch % 4 == 0 ? 4 // common value
		: ( pitch % 2 == 0 ? 2 // dubious efficiency
		:                    1 // least efficient
	) ) );
	
	// GLES2 has no concept of gamma correction or sRGB
	// https://www.khronos.org/registry/OpenGL-Refpages/es2.0/xhtml/glTexImage2D.xml
	glTexImage2D(
		GL_TEXTURE_2D,
		0,
		pixfmts[ channels - 1 ],
		width,
		height,
		0,
		pixfmts[ channels - 1 ],
		GL_UNSIGNED_BYTE,
		data
	);
	#ifdef GLAD_GL
		if( mipmap ) glGenerateMipmapEXT( GL_TEXTURE_2D );
	#else // GLES
		if( mipmap ) glGenerateMipmap( GL_TEXTURE_2D );
	#endif
	
	tex.success = true;
	tex.width = width;
	tex.height = height;
	tex.channels = channels;
	tex.mipmap = mipmap;
	tex.type = GL_TEXTURE_2D;
	
	return tex;
}

Texture loadCubemap( std::vector<GLvoid*> faces, GLsizei width, GLsizei height, unsigned int channels, bool mipmap = true, bool filter = true ){
	Texture tex = newTexture;
	if( faces.size() != 6 ) return tex;

	// GL_LUMINANCE and GL_LUMINANCE_ALPHA do not exist in GLES3 and later.
	GLenum pixfmts[] = {
		GL_LUMINANCE,
		GL_LUMINANCE_ALPHA,
		GL_RGB,
		GL_RGBA
	};

	glGenTextures( 1, &tex.texture );
	glBindTexture( GL_TEXTURE_CUBE_MAP, tex.texture );

	// OpenGL 2.0 does not support seamless cubemaps or GL_TEXTURE_WRAP_R
	glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );

	glTexParameteri(
		GL_TEXTURE_CUBE_MAP,
		GL_TEXTURE_MIN_FILTER,
		mipmap ? ( filter ? GL_LINEAR_MIPMAP_LINEAR : GL_NEAREST_MIPMAP_LINEAR ) : ( filter ? GL_LINEAR : GL_NEAREST )
	);
	glTexParameteri( GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, filter ? GL_LINEAR : GL_NEAREST );

	unsigned int pitch = width * channels;
	glPixelStorei(
		GL_UNPACK_ALIGNMENT,
		    pitch % 8 == 0 ? 8 // most efficient
		: ( pitch % 4 == 0 ? 4 // common value
		: ( pitch % 2 == 0 ? 2 // dubious efficiency
		:                    1 // least efficient
	) ) );

	for( unsigned int i = 0; i < 6; i++ )
		glTexImage2D(
			GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
			0,
			pixfmts[channels - 1],
			width,
			height,
			0,
			pixfmts[channels - 1],
			GL_UNSIGNED_BYTE,
			faces[i]
		);

	#ifdef GLAD_GL
		if( mipmap ) glGenerateMipmapEXT( GL_TEXTURE_CUBE_MAP );
	#else // GLES
		if( mipmap ) glGenerateMipmap( GL_TEXTURE_CUBE_MAP );
	#endif

	tex.success = true;
	tex.width = width;
	tex.height = height;
	tex.channels = channels;
	tex.mipmap = mipmap;
	tex.type = GL_TEXTURE_CUBE_MAP;

	return tex;
}

void freeTexture( Texture &tex ){
	glDeleteTextures( 1, &tex.texture );
	tex = newTexture;
}

void updateTexture( Texture &tex, const GLvoid* data ){
	GLenum pixfmts[] = {
		GL_LUMINANCE,
		GL_LUMINANCE_ALPHA,
		GL_RGB,
		GL_RGBA
	};
	
	glBindTexture( GL_TEXTURE_2D, tex.texture );
	
	glTexSubImage2D(
		GL_TEXTURE_2D,
		0,
		0,
		0,
		tex.width,
		tex.height,
		pixfmts[ tex.channels - 1 ],
		GL_UNSIGNED_BYTE,
		data
	);
	#ifdef GLAD_GL
		if( tex.mipmap ) glGenerateMipmapEXT( GL_TEXTURE_2D );
	#else // GLES
		if( tex.mipmap ) glGenerateMipmap( GL_TEXTURE_2D );
	#endif
}

void updateCubemap( Texture &tex, std::vector<GLvoid*> faces ){
	if( faces.size() != 6 ) return;

	GLenum pixfmts[] = {
		GL_LUMINANCE,
		GL_LUMINANCE_ALPHA,
		GL_RGB,
		GL_RGBA
	};

	glBindTexture( GL_TEXTURE_CUBE_MAP, tex.texture );

	for( unsigned int i = 0; i < 6; i++ )
		glTexSubImage2D(
			GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
			0,
			0,
			0,
			tex.width,
			tex.height,
			pixfmts[ tex.channels - 1 ],
			GL_UNSIGNED_BYTE,
			faces[i]
		);
	#ifdef GLAD_GL
		if( tex.mipmap ) glGenerateMipmapEXT( GL_TEXTURE_CUBE_MAP );
	#else // GLES
		if( tex.mipmap ) glGenerateMipmap( GL_TEXTURE_CUBE_MAP );
	#endif
}

void updateCubemapFace( Texture &tex, const GLvoid* data, unsigned int face ){
	// Faces can be indexed with either integers 0-5 or OpenGL enums.
	face %= GL_TEXTURE_CUBE_MAP_POSITIVE_X;

	GLenum pixfmts[] = {
		GL_LUMINANCE,
		GL_LUMINANCE_ALPHA,
		GL_RGB,
		GL_RGBA
	};

	glBindTexture( GL_TEXTURE_CUBE_MAP, tex.texture );

	glTexSubImage2D(
		face,
		0,
		0,
		0,
		tex.width,
		tex.height,
		pixfmts[ tex.channels - 1 ],
		GL_UNSIGNED_BYTE,
		data
	);
	#ifdef GLAD_GL
		if( tex.mipmap ) glGenerateMipmapEXT( GL_TEXTURE_CUBE_MAP );
	#else // GLES
		if( tex.mipmap ) glGenerateMipmap( GL_TEXTURE_CUBE_MAP );
	#endif
}

void setTexture( Texture tex, GLuint texSlot ){
	if( tex.success ){
		glActiveTexture( GL_TEXTURE0 + texSlot );
		glBindTexture( tex.type, tex.texture );
	}
}

void setFog( Color col ){
	glUniform4f( drawPipeline.u_fog, col.r, col.g, col.b, col.a );
	fogColor = col;
}

void setMetallicFactor( GLfloat f ){
	glUniform1f( drawPipeline.u_metallicFactor, f );
}

void setRoughnessFactor( GLfloat f ){
	glUniform1f( drawPipeline.u_roughnessFactor, f );
}

void setBaseColorFactor( Color col ){
	glUniform4f( drawPipeline.u_baseColorFactor, col.r, col.g, col.b, col.a );
}

void setEmissiveFactor( GLfloat r, GLfloat g, GLfloat b ){
	glUniform3f( drawPipeline.u_emissiveFactor, r, g, b );
}

void setPipeline( Pipeline pipeline ){
	if( pipeline.success ){
		glUseProgram( pipeline.programObject );
		drawPipeline = pipeline;
		glUniform4f( drawPipeline.u_fog, fogColor.r, fogColor.g, fogColor.b, fogColor.a );
		// Bind the sampler locations.
		for( size_t i = 0; i < pipeline.slots.size(); i++ )
			glUniform1i( pipeline.slots[i], i );
	}
}

Pipeline getPipeline(){
	return drawPipeline;
}

void setTextureMatrix( linalg::mat<double,4,4> texMat ){
	texMatrix = texMat;
}

void setLightMatrix( linalg::mat<double,4,4> lightMat ){
	lightMatrix = lightMat;
}

Framebuffer createFramebuffer( GLsizei width, GLsizei height, bool cubemap = false ){
	// This framebuffer API does not currently support:
	// * multisampling (GLES 3.0+)
	// * mipmapping
	
	Framebuffer fb = newFramebuffer;
	#ifdef GLAD_GL
		glGenFramebuffersEXT( 1, &fb.fbo );
		glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, fb.fbo );
	#else // GLES
		glGenFramebuffers( 1, &fb.fbo );
		glBindFramebuffer( GL_FRAMEBUFFER, fb.fbo );
	#endif
	
	fb.texture_type = cubemap ? GL_TEXTURE_CUBE_MAP : GL_TEXTURE_2D;
	glGenTextures( 1, &fb.texture );
	glBindTexture( fb.texture_type, fb.texture );
	
	if( cubemap ){
		// Create 6 blank faces.
		for( unsigned int i = 0; i < 6; i++ ){
			glTexImage2D(
				GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
				0,
				GL_RGB,
				width,
				height,
				0,
				GL_RGB,
				GL_UNSIGNED_BYTE,
				nullptr
			);
			#ifdef GLAD_GL
				glFramebufferTexture2DEXT(
					GL_FRAMEBUFFER_EXT,
					GL_COLOR_ATTACHMENT0_EXT,
					GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
					fb.texture,
					0
				);
			#else
				glFramebufferTexture2D(
					GL_FRAMEBUFFER,
					GL_COLOR_ATTACHMENT0,
					GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
					fb.texture,
					0
				);
			#endif
		}
	}else{
		// Create a blank surface.
		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr );
		#ifdef GLAD_GL
			glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, fb.texture, 0 );
		#else
			glFramebufferTexture2D( GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb.texture, 0 );
		#endif
	}
	
	glTexParameteri( fb.texture_type, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
	glTexParameteri( fb.texture_type, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
	glTexParameteri( fb.texture_type, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
	glTexParameteri( fb.texture_type, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
	// Unbind the texture.
	glBindTexture( fb.texture_type, 0 );
	
	if( cubemap ){
		// Cubemap depth textures are unsupported.
		fb.texture_z = 0;
		// Create a depth renderbuffer.
		#ifdef GLAD_GL
			glGenRenderbuffersEXT( 1, &fb.rbo );
			glBindRenderbufferEXT( GL_RENDERBUFFER_EXT, fb.rbo );
			glRenderbufferStorageEXT( GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT16, width, height );
			glFramebufferRenderbufferEXT( GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, fb.rbo );
		#else // GLES
			glGenRenderbuffers( 1, &fb.rbo );
			glBindRenderbuffer( GL_RENDERBUFFER, fb.rbo );
			glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height );
			glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fb.rbo );
		#endif
	}else{
		fb.rbo = 0;
		// Create a depth texture.
		glGenTextures( 1, &fb.texture_z );
		glBindTexture( GL_TEXTURE_2D, fb.texture_z );
		//glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, nullptr );
		glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, nullptr );
		#ifdef GLAD_GL
			glFramebufferTexture2DEXT( GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, fb.texture_z, 0 );
		#else // GLES
			glFramebufferTexture2D( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, fb.texture_z, 0 );
		#endif
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );
		glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST );
		// Unbind the texture.
		glBindTexture( GL_TEXTURE_2D, 0 );
	}
	
	fb.success =
	#ifdef GLAD_GL
		glCheckFramebufferStatusEXT( GL_FRAMEBUFFER_EXT ) == GL_FRAMEBUFFER_COMPLETE_EXT;
	#else // GLES
		glCheckFramebufferStatus( GL_FRAMEBUFFER ) == GL_FRAMEBUFFER_COMPLETE;
	#endif
	
	fb.width = width;
	fb.height = height;
	return fb;
}

void resizeFramebuffer( Framebuffer &fb, GLsizei width, GLsizei height ){
	// Only resize if necessary.
	if( fb.width == width && fb.height == height ) return;

	#ifdef GLAD_GL
		glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, fb.fbo );
	#else // GLES
		glBindFramebuffer( GL_FRAMEBUFFER, fb.fbo );
	#endif

	glBindTexture( fb.texture_type, fb.texture );

	if( fb.texture_type == GL_TEXTURE_CUBE_MAP ){
		// Resize the 6 faces.
		for( unsigned int i = 0; i < 6; i++ ){
			glTexImage2D(
				GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
				0,
				GL_RGB,
				width,
				height,
				0,
				GL_RGB,
				GL_UNSIGNED_BYTE,
				nullptr
			);
		}
	}else{
		// Resize the texture.
		glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr );
	}

	// Unbind the texture.
	glBindTexture( fb.texture_type, 0 );

	if( fb.texture_type == GL_TEXTURE_CUBE_MAP ){
		// Replace the depth renderbuffer.
		#ifdef GLAD_GL
			glDeleteRenderbuffersEXT( 1, &fb.rbo );
			glGenRenderbuffersEXT( 1, &fb.rbo );
			glBindRenderbufferEXT( GL_RENDERBUFFER_EXT, fb.rbo );
			glRenderbufferStorageEXT( GL_RENDERBUFFER_EXT, GL_DEPTH_COMPONENT16, width, height );
			glFramebufferRenderbufferEXT( GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, fb.rbo );
		#else // GLES
			glDeleteRenderbuffers( 1, &fb.rbo );
			glGenRenderbuffers( 1, &fb.rbo );
			glBindRenderbuffer( GL_RENDERBUFFER, fb.rbo );
			glRenderbufferStorage( GL_RENDERBUFFER, GL_DEPTH_COMPONENT16, width, height );
			glFramebufferRenderbuffer( GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fb.rbo );
		#endif
	}else{
		// Resize the depth texture.
		glBindTexture( GL_TEXTURE_2D, fb.texture_z );
		//glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT16, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, nullptr );
		glTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, width, height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, nullptr );
		// Unbind the texture.
		glBindTexture( GL_TEXTURE_2D, 0 );
	}

	fb.success =
	#ifdef GLAD_GL
		glCheckFramebufferStatusEXT( GL_FRAMEBUFFER_EXT ) == GL_FRAMEBUFFER_COMPLETE_EXT;
	#else // GLES
		glCheckFramebufferStatus( GL_FRAMEBUFFER ) == GL_FRAMEBUFFER_COMPLETE;
	#endif

	fb.width = width;
	fb.height = height;
}

Texture getFramebufferTexture( Framebuffer &fb ){
	Texture tex = newTexture;
	tex.success  = fb.success;
	tex.texture  = fb.texture;
	tex.width    = fb.width;
	tex.height   = fb.height;
	tex.channels = 4;
	tex.mipmap   = false; // TODO: mipmapping
	tex.type     = fb.texture_type;
	return tex;
}

// Used by setFramebuffer.
Display getDisplay();

void setFramebuffer( Framebuffer fb = newFramebuffer ){
	// Set the drawing target to the framebuffer or the screen.
	if( fb.success ){
		#ifdef GLAD_GL
			glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, fb.fbo );
		#else // GLES
			glBindFramebuffer( GL_FRAMEBUFFER, fb.fbo );
		#endif
		glViewport( 0, 0, fb.width, fb.height );
	}else{
		#ifdef GLAD_GL
			glBindFramebufferEXT( GL_FRAMEBUFFER_EXT, 0 );
		#else // GLES
			glBindFramebuffer( GL_FRAMEBUFFER, 0 );
		#endif
		auto disp = getDisplay();
		glViewport( 0, 0, disp.width, disp.height );
	}
}

void drawFramebuffer( Framebuffer &fb, bool draw_z = false ){
	if( !fb.success ) return;
	glDisable( GL_DEPTH_TEST );
	glDisable( GL_CULL_FACE );
	glActiveTexture( GL_TEXTURE0 );
	glBindTexture( fb.texture_type, draw_z ? fb.texture_z : fb.texture );
	texMatrix = linalg::identity;
	// Stretch to screen bounds and flip vertically.
	drawMesh(
		planeMesh,
		linalg::scaling_matrix( linalg::vec<double,3>( 2.0, -2.0, 0.0 ) ),
		linalg::identity,
		linalg::identity
	);
	glEnable( GL_DEPTH_TEST );
	glEnable( GL_CULL_FACE );
}

Framebuffer getIrradianceFramebuffer( Texture in_cubemap, Framebuffer fb = newFramebuffer ){
	if( !fb.success ) fb = createFramebuffer( 32, 32, true );
	setFramebuffer( fb );

	// Degrees to radians.
	const double d2r = 0.01745329251994329577;

	linalg::vec<double,3> viewAngles[] = {
		{   0.0, -90.0, 180.0 }, // Right
		{   0.0,  90.0, 180.0 }, // Left
		{  90.0,   0.0,   0.0 }, // Top
		{ -90.0,   0.0,   0.0 }, // Bottom
		{   0.0, 180.0, 180.0 }, // Back
		{   0.0,   0.0, 180.0 }  // Front
	};

	linalg::mat<double,4,4> projMat = linalg::perspective_matrix( 90.0 * d2r, 1.0, 0.1, 10.0 );

	auto old_pipeline = drawPipeline;
	setPipeline( irradiancePipeline );
	glDisable( GL_BLEND );
	glDisable( GL_CULL_FACE );
	setTexture( in_cubemap, 0 );

	for( unsigned int i = 0; i < 6; i++ ){
		#ifdef GLAD_GL
			glFramebufferTexture2DEXT(
				GL_FRAMEBUFFER_EXT,
				GL_COLOR_ATTACHMENT0_EXT,
				GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
				fb.texture,
				0
			);
		#else // GLES
			glFramebufferTexture2D(
				GL_FRAMEBUFFER,
				GL_COLOR_ATTACHMENT0,
				GL_TEXTURE_CUBE_MAP_POSITIVE_X + i,
				fb.texture,
				0
			);
		#endif
		glClear( GL_DEPTH_BUFFER_BIT );
		drawMesh(
			cubeMesh,
			linalg::identity,
			linalg::rotation_matrix(
				eulerToQuat(
					viewAngles[i].x * d2r,
					viewAngles[i].y * d2r,
					viewAngles[i].z * d2r
				)
			),
			projMat
		);
	}

	setFramebuffer();
	setPipeline( old_pipeline );
	glEnable( GL_CULL_FACE );
	return fb;
}

void drawSkybox( Texture &tex, linalg::mat<double,4,4> view, linalg::mat<double,4,4> proj, Color tint = newColor ){
	if( !skyboxPipeline.success ) return;
	auto old_pipeline = drawPipeline;
	setPipeline( skyboxPipeline );
	glUniform4f( drawPipeline.u_fog, tint.r, tint.g, tint.b, tint.a );
	glDisable( GL_CULL_FACE );
	setTexture( tex, 0 );
	// Extract the 3x3 rotation matrix from the view matrix.
	// The camera is effectively at <0,0,0> with the skybox.
	drawMesh(
		cubeMesh,
		linalg::identity,
		linalg::mat<double,4,4>(
			{ view[0][0], view[0][1], view[0][2], 0.0 },
			{ view[1][0], view[1][1], view[1][2], 0.0 },
			{ view[2][0], view[2][1], view[2][2], 0.0 },
			{ 0.0,        0.0,        0.0,        1.0 }
		),
		proj
	);
	glUniform4f( drawPipeline.u_fog, fogColor.r, fogColor.g, fogColor.b, fogColor.a );
	setPipeline( old_pipeline );
	glEnable( GL_CULL_FACE );
}

int compileShader( GLuint shader ){
	glCompileShader( shader );
	GLint compiled;
	glGetShaderiv( shader, GL_COMPILE_STATUS, &compiled );
	if( !compiled ){
		fprintf( stderr, "Failed to compile shader.\n" );
		GLint infoLen = 0;
		glGetShaderiv( shader, GL_INFO_LOG_LENGTH, &infoLen );
		if( infoLen > 1 ){
			char* infoLog = new char[infoLen];
			glGetShaderInfoLog( shader, infoLen, nullptr, infoLog );
			fprintf( stderr, "%s\n", infoLog );
			delete[] infoLog;
		}
		glDeleteShader( shader );
		return 0;
	}
	GLint debugout;
	glGetShaderiv( shader, GL_SHADER_TYPE, &debugout );
	if( verbose ){
		printf( "%s\n", ( debugout == GL_VERTEX_SHADER ? "VERTEX SHADER" : "FRAGMENT SHADER" ) );
		printf( "Shader address:         %d\n", shader );
	}
	glGetShaderiv( shader, GL_SHADER_SOURCE_LENGTH, &debugout );
	if( verbose ) printf( "Shader source length:   %d\n\n", debugout );
	return 1;
}

int linkProgram( GLuint programObject ){
	glLinkProgram( programObject );
	GLint infoLen = 0;
	GLint linked;
	glGetProgramiv( programObject, GL_LINK_STATUS, &linked );
	if( !linked ){
		fprintf( stderr, "Failed to link shader program.\n" );
		glGetProgramiv( programObject, GL_INFO_LOG_LENGTH, &infoLen );
		if( infoLen > 1 ){
			char* infoLog = new char[infoLen];
			glGetProgramInfoLog( programObject, infoLen, nullptr, infoLog );
			fprintf( stderr, "%s\n", infoLog );
			delete[] infoLog;
		}
		glDeleteProgram( programObject );
		return 0;
	}
	if( verbose ){
		printf( "PROGRAM OBJECT\n" );
		printf( "Program object address: %d\n", programObject );
	}
	GLint debugout;
	glGetProgramiv( programObject, GL_ATTACHED_SHADERS, &debugout );
	if( verbose ) printf( "Attached shaders:       %d\n", debugout );
	glGetProgramiv( programObject, GL_ACTIVE_ATTRIBUTES, &debugout );
	if( verbose ) printf( "Active attributes:      %d\n", debugout );
	glGetProgramiv( programObject, GL_ACTIVE_UNIFORMS, &debugout );
	if( verbose ) printf( "Active uniforms:        %d\n", debugout );
	
	glValidateProgram( programObject );
	glGetProgramiv( programObject, GL_VALIDATE_STATUS, &debugout );
	if( verbose )
		printf( "%s\n", ( debugout == GL_TRUE ? "Program object is valid." : "Program object is not valid." ) );
	glGetProgramiv( programObject, GL_INFO_LOG_LENGTH, &infoLen );
	if( infoLen > 1 ){
		char* infoLog = new char[infoLen];
		glGetProgramInfoLog( programObject, infoLen, nullptr, infoLog );
		if( verbose ) printf( "%s\n", infoLog );
		delete[] infoLog;
	}
	
	printf( "\n" );
	
	return 1;
}

Pipeline loadPipeline( const char* vertSrc, const char* fragSrc, std::vector<std::string> samplers = {} ){
	Pipeline pipeline = newPipeline;
	
	GLuint vert;
	if( ( vert = glCreateShader( GL_VERTEX_SHADER ) ) == 0 ) return pipeline;
	const char* vertStrings[] = { shaderHeader, vertSrc };
	glShaderSource( vert, 2, vertStrings, nullptr );
	if( !compileShader( vert ) ) return pipeline;
	
	GLuint frag;
	if( ( frag = glCreateShader( GL_FRAGMENT_SHADER ) ) == 0 ) return pipeline;
	const char* fragStrings[] = { shaderHeader, fragSrc };
	glShaderSource( frag, 2, fragStrings, nullptr );
	if( !compileShader( frag ) ) return pipeline;
	
	GLuint programObject;
	if( ( programObject = glCreateProgram() ) == 0 ) return pipeline;
	glAttachShader( programObject, vert );
	glAttachShader( programObject, frag );
	
	// only needed if layout qualifier is not used (like in OpenGL 2.0)
	glBindAttribLocation( programObject, 0, "a_Position" );
	glBindAttribLocation( programObject, 1, "a_Normal" );
	glBindAttribLocation( programObject, 2, "a_Tangent" );
	glBindAttribLocation( programObject, 3, "a_UV" );
	
	if( !linkProgram( programObject ) ) return pipeline;
	
	// https://stackoverflow.com/questions/39784072/is-there-garbage-collection-on-the-gpu
	// https://gamedev.stackexchange.com/questions/47910/after-a-succesful-gllinkprogram-should-i-delete-detach-my-shaders
	glDetachShader( programObject, vert );
	glDeleteShader( vert );
	glDetachShader( programObject, frag );
	glDeleteShader( frag );
	
	pipeline.success           = true;
	pipeline.programObject     = programObject;
	pipeline.u_metallicFactor  = glGetUniformLocation( programObject, "u_metallicFactor" );
	pipeline.u_roughnessFactor = glGetUniformLocation( programObject, "u_roughnessFactor" );
	pipeline.u_baseColorFactor = glGetUniformLocation( programObject, "u_baseColorFactor" );
	pipeline.u_emissiveFactor  = glGetUniformLocation( programObject, "u_emissiveFactor" );
	pipeline.u_matrices        = glGetUniformLocation( programObject, "u_matrices" );
	pipeline.u_fog             = glGetUniformLocation( programObject, "u_fog" );
	pipeline.u_camera          = glGetUniformLocation( programObject, "u_camera" );
	
	// Add the sampler locations to be bound in setPipeline.
	for( size_t i = 0; i < samplers.size(); i++ )
		pipeline.slots.push_back( glGetUniformLocation( programObject, samplers[i].c_str() ) );
	
	return pipeline;
}

Display createDisplay( unsigned int width, unsigned int height, std::string title, int multisamples = 4, bool HiDPI = false, bool vsync = true ){
	// Opens a window with a GL context.
	// Display display = createDisplay( 800, 600, "Title" );

	Display disp = newDisplay;
	
	if( SDL_Init( SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC ) < 0 ){
		fprintf( stderr, "Failed to initialize SDL.\n" );
		return disp;
	}
	
	if( verbose ) printf( "Creating GL 2 window...\n" );
	
	SDL_GL_LoadLibrary( nullptr );
	
	#ifdef GLAD_GL
		SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_COMPATIBILITY );
	#else // GLES
		SDL_GL_SetAttribute( SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES );
	#endif
	SDL_GL_SetAttribute( SDL_GL_CONTEXT_MAJOR_VERSION, 2 );
	SDL_GL_SetAttribute( SDL_GL_CONTEXT_MINOR_VERSION, 0 );
	
	if( multisamples > 1 ){
		SDL_GL_SetAttribute( SDL_GL_MULTISAMPLEBUFFERS, 1 );
		SDL_GL_SetAttribute( SDL_GL_MULTISAMPLESAMPLES, multisamples );
	}else{
		#ifdef __EMSCRIPTEN__
			EmscriptenWebGLContextAttributes att;
			emscripten_webgl_init_context_attributes( &att );
			att.antialias = EM_FALSE;
			att.majorVersion = 1;
			att.minorVersion = 0;
			emscripten_webgl_make_context_current( emscripten_webgl_create_context( 0, &att ) );
		#endif
	}
	
	#ifdef __EMSCRIPTEN__
	// Emscripten's fixed-resolution canvas doesn't always scale properly.
	// Force full window.
	if( true ){
	#else
	if( width == 0 && height == 0 ){
	#endif
		window = SDL_CreateWindow(
			title.c_str(),
			SDL_WINDOWPOS_UNDEFINED,
			SDL_WINDOWPOS_UNDEFINED,
			0,
			0,
			HiDPI ?
			( SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_OPENGL | SDL_WINDOW_ALLOW_HIGHDPI ) :
			( SDL_WINDOW_FULLSCREEN_DESKTOP | SDL_WINDOW_OPENGL )
		);
		#ifdef __EMSCRIPTEN__
			SDL_SetWindowFullscreen( window, 0 ); // Complicated reasons for needing this.
			EmscriptenFullscreenStrategy strat;
			strat.scaleMode = EMSCRIPTEN_FULLSCREEN_SCALE_STRETCH;
			strat.canvasResolutionScaleMode =
			HiDPI ?
			EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF :
			EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_STDDEF;
			strat.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_NEAREST;
			emscripten_enter_soft_fullscreen( 0, &strat );
			width  = (unsigned int)FG2_GET_CANVAS_WIDTH;
			height = (unsigned int)FG2_GET_CANVAS_HEIGHT;
		#else
			int winw, winh;
			SDL_GetWindowSize( window, &winw, &winh );
			width  = (unsigned int)winw;
			height = (unsigned int)winh;
		#endif
	}else{
		window = SDL_CreateWindow(
			title.c_str(),
			SDL_WINDOWPOS_CENTERED,
			SDL_WINDOWPOS_CENTERED,
			width,
			height,
			HiDPI ?
			( SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI ) :
			( SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE )
		);
	}
	if( !window ){
		fprintf( stderr, "Failed to create GL window.\n" );
		return disp;
	}
	ctx = SDL_GL_CreateContext( window );
	if( !ctx ){
		fprintf( stderr, "Failed to create GL context.\n" );
		return disp;
	}
	if( vsync ){
		// Try to set up late swap tearing or vsync.
		if( SDL_GL_SetSwapInterval( -1 ) < 0 )
			if( SDL_GL_SetSwapInterval( 1 ) < 0 ) SDL_GL_SetSwapInterval( 0 );
	}else{
		// No vsync.
		SDL_GL_SetSwapInterval( 0 );
	}
	
	windowID = SDL_GetWindowID( window );
	
	#if defined(GLAD_GL)
		int version = gladLoadGL( (GLADloadfunc)SDL_GL_GetProcAddress );
	#elif defined(__EMSCRIPTEN__)
		// Work around a regression in SDL 2.0.12.
		int version = gladLoadGLES2( (GLADloadfunc)emscripten_webgl1_get_proc_address );
	#else
		int version = gladLoadGLES2( (GLADloadfunc)SDL_GL_GetProcAddress );
	#endif
	
	if( version == 0 ){
		fprintf( stderr, "Failed to use extension loader.\n" );
		return disp;
	}
	
	if( verbose ){
		printf( "Success!\n" );
		printf( "OpenGL vendor:   %s\n", glGetString( GL_VENDOR ) );
		printf( "OpenGL renderer: %s\n", glGetString( GL_RENDERER ) );
		printf( "OpenGL version:  %s\n", glGetString( GL_VERSION ) );
		printf( "GLSL version:    %s\n\n", glGetString( GL_SHADING_LANGUAGE_VERSION ) );
	}
	
	glEnable( GL_CULL_FACE );
	glEnable( GL_DEPTH_TEST );
	glDepthFunc( GL_LEQUAL );
	glDepthMask( GL_TRUE );
	if( multisamples > 1 ) glEnable( GL_SAMPLE_ALPHA_TO_COVERAGE );
	
	unlitPipeline = loadPipeline( unlitVert, unlitFrag, unlitSamplers );
	if( unlitPipeline.success ){
		if( verbose ) printf( "Unlit pipeline loaded successfully.\n\n" );
	}else{
		fprintf( stderr, "Failed to load unlit pipeline.\n\n" );
		return disp;
	}
	setPipeline( unlitPipeline );
	
	colorModPipeline = loadPipeline( unlitVert, colorModFrag, colorModSamplers );
	if( colorModPipeline.success ){
		if( verbose ) printf( "Color mod pipeline loaded successfully.\n\n" );
	}else{
		fprintf( stderr, "Failed to load color mod pipeline.\n\n" );
		return disp;
	}
	
	skyboxPipeline = loadPipeline( skyboxVert, skyboxFrag, skyboxSamplers );
	if( skyboxPipeline.success ){
		if( verbose ) printf( "Skybox pipeline loaded successfully.\n\n" );
	}else{
		fprintf( stderr, "Failed to load skybox pipeline.\n\n" );
	}
	
	irradiancePipeline = loadPipeline( skyboxVert, irradianceFrag, irradianceSamplers );
	if( irradiancePipeline.success ){
		if( verbose ) printf( "Irradiance pipeline loaded successfully.\n\n" );
	}else{
		fprintf( stderr, "Failed to load irradiance pipeline.\n\n" );
	}
	
	unsigned char pixels[] = { 0xFF, 0xFF, 0xFF, 0xFF };
	
	blankTexture = loadTexture( pixels, 2, 2, 1, false, false );
	if( blankTexture.success ){
		if( verbose ) printf( "Blank texture loaded successfully.\n\n" );
	}else{
		fprintf( stderr, "Failed to load blank texture.\n\n" );
		return disp;
	}
	
	std::vector<Vertex> vertices;
	std::vector<Index> indices;
	
	vertices = {
		{ { -0.5,  0.5,  0.0 }, { 0.0, 0.0, 1.0 }, { 1.0, 0.0, 0.0 }, { 0.0, 0.0 } },
		{ { -0.5, -0.5,  0.0 }, { 0.0, 0.0, 1.0 }, { 1.0, 0.0, 0.0 }, { 0.0, 1.0 } },
		{ {  0.5, -0.5,  0.0 }, { 0.0, 0.0, 1.0 }, { 1.0, 0.0, 0.0 }, { 1.0, 1.0 } },
		{ {  0.5,  0.5,  0.0 }, { 0.0, 0.0, 1.0 }, { 1.0, 0.0, 0.0 }, { 1.0, 0.0 } }
	};
	
	indices = { 0, 1, 2,  2, 3, 0 };
	
	planeMesh = loadMesh( vertices, indices );
	if( planeMesh.success ){
		if( verbose ) printf( "Plane mesh loaded successfully.\n\n" );
	}else{
		fprintf( stderr, "Failed to load plane mesh.\n\n" );
		return disp;
	}
	
	vertices = {
		{ { -0.5,  0.5,  0.5 }, {  0.0,  0.0,  1.0 }, {  1.0,  0.0,  0.0 }, { 0.0, 0.0 } },
		{ { -0.5, -0.5,  0.5 }, {  0.0,  0.0,  1.0 }, {  1.0,  0.0,  0.0 }, { 0.0, 1.0 } },
		{ {  0.5, -0.5,  0.5 }, {  0.0,  0.0,  1.0 }, {  1.0,  0.0,  0.0 }, { 1.0, 1.0 } },
		{ {  0.5,  0.5,  0.5 }, {  0.0,  0.0,  1.0 }, {  1.0,  0.0,  0.0 }, { 1.0, 0.0 } },
		{ {  0.5,  0.5,  0.5 }, {  1.0,  0.0,  0.0 }, {  0.0,  0.0, -1.0 }, { 0.0, 0.0 } },
		{ {  0.5, -0.5,  0.5 }, {  1.0,  0.0,  0.0 }, {  0.0,  0.0, -1.0 }, { 0.0, 1.0 } },
		{ {  0.5, -0.5, -0.5 }, {  1.0,  0.0,  0.0 }, {  0.0,  0.0, -1.0 }, { 1.0, 1.0 } },
		{ {  0.5,  0.5, -0.5 }, {  1.0,  0.0,  0.0 }, {  0.0,  0.0, -1.0 }, { 1.0, 0.0 } },
		{ { -0.5,  0.5, -0.5 }, { -1.0,  0.0,  0.0 }, {  0.0,  0.0,  1.0 }, { 0.0, 0.0 } },
		{ { -0.5, -0.5, -0.5 }, { -1.0,  0.0,  0.0 }, {  0.0,  0.0,  1.0 }, { 0.0, 1.0 } },
		{ { -0.5, -0.5,  0.5 }, { -1.0,  0.0,  0.0 }, {  0.0,  0.0,  1.0 }, { 1.0, 1.0 } },
		{ { -0.5,  0.5,  0.5 }, { -1.0,  0.0,  0.0 }, {  0.0,  0.0,  1.0 }, { 1.0, 0.0 } },
		{ {  0.5,  0.5, -0.5 }, {  0.0,  0.0, -1.0 }, { -1.0,  0.0,  0.0 }, { 0.0, 0.0 } },
		{ {  0.5, -0.5, -0.5 }, {  0.0,  0.0, -1.0 }, { -1.0,  0.0,  0.0 }, { 0.0, 1.0 } },
		{ { -0.5, -0.5, -0.5 }, {  0.0,  0.0, -1.0 }, { -1.0,  0.0,  0.0 }, { 1.0, 1.0 } },
		{ { -0.5,  0.5, -0.5 }, {  0.0,  0.0, -1.0 }, { -1.0,  0.0,  0.0 }, { 1.0, 0.0 } },
		{ { -0.5,  0.5, -0.5 }, {  0.0,  1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 1.0, 1.0 } },
		{ { -0.5,  0.5,  0.5 }, {  0.0,  1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 1.0, 0.0 } },
		{ {  0.5,  0.5,  0.5 }, {  0.0,  1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 0.0, 0.0 } },
		{ {  0.5,  0.5, -0.5 }, {  0.0,  1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 0.0, 1.0 } },
		{ { -0.5, -0.5,  0.5 }, {  0.0, -1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 0.0, 0.0 } },
		{ { -0.5, -0.5, -0.5 }, {  0.0, -1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 0.0, 1.0 } },
		{ {  0.5, -0.5, -0.5 }, {  0.0, -1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 1.0, 1.0 } },
		{ {  0.5, -0.5,  0.5 }, {  0.0, -1.0,  0.0 }, {  1.0,  0.0,  0.0 }, { 1.0, 0.0 } }
	};
	
	indices = {
		 0,  1,  2,   2,  3,  0,
		 4,  5,  6,   6,  7,  4,
		 8,  9, 10,  10, 11,  8,
		12, 13, 14,  14, 15, 12,
		16, 17, 18,  18, 19, 16,
		20, 21, 22,  22, 23, 20
	};
	
	cubeMesh = loadMesh( vertices, indices );
	if( cubeMesh.success ){
		if( verbose ) printf( "Cube mesh loaded successfully.\n\n" );
	}else{
		fprintf( stderr, "Failed to load cube mesh.\n\n" );
		return disp;
	}
	
	// Avoid a segfault when keys are accessed before the first sync.
	keystates = SDL_GetKeyboardState( nullptr );
	
	#ifndef __EMSCRIPTEN__
		// Attempt to load the controller database file.
		const char* mappings_file = "gamecontrollerdb.txt";
		if( SDL_GameControllerAddMappingsFromFile( mappings_file ) < 0 ){
			fprintf( stderr, "Failed to load %s: %s\n", mappings_file, SDL_GetError() );
		}
	#endif
	
	// Open the first available controller.
	for( int i = 0; i < SDL_NumJoysticks(); i++ ){
		if( SDL_IsGameController( i ) ){
			controller = SDL_GameControllerOpen( i );
			if( controller ){
				// Open the controller's haptic device.
				haptic = SDL_HapticOpenFromJoystick( SDL_GameControllerGetJoystick( controller ) );
				// Verify rumble support.
				if( SDL_HapticRumbleInit( haptic ) < 0 ){
					SDL_HapticClose( haptic );
					haptic = nullptr;
				}
				break;
			}else{
				fprintf( stderr, "Failed to open game controller %d: %s\n", i, SDL_GetError() );
			}
		}
	}
	
	// SDL cannot fill in mouse position until the mouse moves.
	
	disp.success = true;
	disp.width = width;
	disp.height = height;
	disp.title = title;
	return disp;
}

Display getDisplay(){
	Display disp = newDisplay;
	#ifdef __EMSCRIPTEN__
		int width = FG2_GET_CANVAS_WIDTH, height = FG2_GET_CANVAS_HEIGHT;
	#else
		int width, height;
		SDL_GetWindowSize( window, &width, &height );
	#endif
	disp.success = width > 0 ? true : false;
	disp.width = (unsigned int)width;
	disp.height = (unsigned int)height;
	if( disp.success ) disp.title = SDL_GetWindowTitle( window );	
	return disp;
}

unsigned int getDisplayWidth(){
	#ifdef __EMSCRIPTEN__
		return (unsigned int)FG2_GET_CANVAS_WIDTH;
	#else
		int width;
		SDL_GetWindowSize( window, &width, nullptr );
		return (unsigned int)width;
	#endif
}

unsigned int getDisplayHeight(){
	#ifdef __EMSCRIPTEN__
		return (unsigned int)FG2_GET_CANVAS_HEIGHT;
	#else
		int height;
		SDL_GetWindowSize( window, nullptr, &height );
		return (unsigned int)height;
	#endif
}

double deltaTime(){
	Uint64 last = now;
	now = SDL_GetPerformanceCounter();
	return ( now - last ) / (double)SDL_GetPerformanceFrequency();
}

// Used by syncEvents.
void end();

void syncEvents(){
	int sdlWidth, sdlHeight; // Not always the actual width and height.
	#ifdef __EMSCRIPTEN__
		SDL_GL_GetDrawableSize( window, &sdlWidth, &sdlHeight );
		int canvasWidth = FG2_GET_CANVAS_WIDTH, canvasHeight = FG2_GET_CANVAS_HEIGHT;
		EmscriptenMouseEvent mouseState;
		if( emscripten_get_mouse_status( &mouseState ) == EMSCRIPTEN_RESULT_SUCCESS ){
			mouseX = mouseState.canvasX;
			mouseY = mouseState.canvasY;
		}
	#endif
	// Used for touch scaling.
	double screenWidth = getDisplayWidth(), screenHeight = getDisplayHeight();
	mouseMoveX = 0;
	mouseMoveY = 0;
	mouseWheel = 0;
	touchStart = false;
	textReturnStart = false;
	SDL_Event event = {};
	while( SDL_PollEvent( &event ) ){
		if( event.type == SDL_MOUSEMOTION ){
			#ifdef __EMSCRIPTEN__ // canvas[Width|Height] != sdl[Width|Height], distorting mouse movement
				mouseMoveX += (double)canvasWidth / (double)sdlWidth * (double)event.motion.xrel;
				mouseMoveY += (double)canvasHeight / (double)sdlHeight * (double)event.motion.yrel;
			#else
				mouseMoveX += event.motion.xrel;
				mouseMoveY += event.motion.yrel;
				mouseX = event.motion.x;
				mouseY = event.motion.y;
			#endif
		}else if( event.type == SDL_MOUSEWHEEL ){
			mouseWheel = event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED
				? event.wheel.y * -1 : event.wheel.y;
		}else if( event.type == SDL_FINGERDOWN
			|| event.type == SDL_FINGERMOTION ){
			// TODO: mouseMoveX, mouseMoveY
			mouseX = event.tfinger.x * screenWidth;
			mouseY = event.tfinger.y * screenHeight;
			touchPressure = event.tfinger.pressure;
			if( event.type == SDL_FINGERDOWN ) touchStart = true;
		}else if( event.type == SDL_FINGERUP ){
			touchPressure = 0.0f;
		}else if( event.type == SDL_QUIT ){
			end();
		}else if( event.type == SDL_WINDOWEVENT
			&& event.window.windowID == windowID ){
			// Window events.
			if( event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED ){
				#ifdef __EMSCRIPTEN__
					glViewport( 0, 0, canvasWidth, canvasHeight );
				#else
					SDL_GL_GetDrawableSize( window, &sdlWidth, &sdlHeight );
					glViewport( 0, 0, sdlWidth, sdlHeight );
				#endif
			}else if( event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED ){
				hasFocus = true;
			}else if( event.window.event == SDL_WINDOWEVENT_FOCUS_LOST ){
				hasFocus = false;
			}
		}else if( textInputEnabled && event.type == SDL_TEXTINPUT ){
			// Text input from keyboard or IME.
			textInputString += event.text.text;
		}else if( textInputEnabled && event.type == SDL_KEYDOWN
			&& event.key.keysym.sym == SDLK_BACKSPACE
			&& textInputString.length() > 0 ){
			// Backspace deletion.
			textInputString.pop_back();
		}else if( textInputEnabled && event.type == SDL_KEYDOWN
			&& ( event.key.keysym.sym == SDLK_RETURN
				|| event.key.keysym.sym == SDLK_KP_ENTER ) ){
			// For handling the Enter key during text input.
			textReturnStart = true;
		}
	}
	keystates = SDL_GetKeyboardState( nullptr );
	// Handle controller hotplugging.
	if( !controller || !SDL_GameControllerGetAttached( controller ) ){
		controller = nullptr;
		if( haptic ){
			// Clean up haptic device.
			SDL_HapticClose( haptic );
			haptic = nullptr;
		}
		// Open the first available controller.
		for( int i = 0; i < SDL_NumJoysticks(); i++ ){
			if( SDL_IsGameController( i ) ){
				// Open the controller.
				controller = SDL_GameControllerOpen( i );
				if( controller ){
					// Open the controller's haptic device.
					haptic = SDL_HapticOpenFromJoystick( SDL_GameControllerGetJoystick( controller ) );
					// Verify rumble support.
					if( SDL_HapticRumbleInit( haptic ) < 0 ){
						SDL_HapticClose( haptic );
						haptic = nullptr;
					}
					break;
				}
			}
		}
	}
}

void sync(){
	SDL_GL_SwapWindow( window );
	syncEvents();
}

void setTextInput( int x = 0, int y = 0, int w = 0, int h = 0 ){
	// https://wiki.libsdl.org/Tutorials/TextInput
	if( x || y || w || h ){
		textInputRect = { x, y, w, h };
		#ifdef __EMSCRIPTEN__
			// Add a text input element.
			EM_ASM( {
				// Helper form.
				var form = document.getElementById( "FFORM" );
				if( form == null ){
					form = document.createElement( "form" );
					form.id = "FFORM";
					form.action = "/";
					form.style.position = "absolute";
					form.style.left = "0px";
					form.style.top = "0px";
					form.style.margin = 0;
					form.style.border = 0;
					form.style.padding = 0;
					document.body.appendChild( form );
					var submit = document.createElement( "input" );
					submit.type = "submit";
					submit.style.display = "none";
					form.appendChild( submit );
					form.onsubmit = function( e ){ e.preventDefault(); };
				}
				// Input field.
				var ti = document.getElementById( "FTEXTINPUT" );
				if( ti == null ){
					ti = document.createElement( "input" );
					ti.type = "text";
					ti.id = "FTEXTINPUT";
					ti.style.textAlign = "center";
					ti.style.position = "absolute";
					ti.style.zIndex = 99;
					ti.style.margin = 0;
					ti.style.border = 0;
					ti.style.padding = 0;
					ti.style.opacity = 0.0;
					form.appendChild( ti );
					ti.focus();
				}
				var canv = Module.canvas;
				var scale = canv.offsetHeight / canv.height;
				ti.style.left = ( $0 * scale + canv.offsetLeft ) + "px";
				ti.style.top = ( $1 * scale + canv.offsetTop ) + "px";
				ti.style.width = ( $2 * scale ) + "px";
				ti.style.height = ( $3 * scale ) + "px";
			}, x, y, w, h );
		#else
			if( !SDL_IsTextInputActive() )
				SDL_StartTextInput();
			SDL_SetTextInputRect( &textInputRect );
		#endif
		textInputEnabled = true;
	}else{
		#ifdef __EMSCRIPTEN__
			// Remove the text input element.
			EM_ASM( {
				var form = document.getElementById( "FFORM" );
				var ti = document.getElementById( "FTEXTINPUT" );
				if( ti != null ) form.removeChild( ti );
			} );
		#else
			if( SDL_IsTextInputActive() )
				SDL_StopTextInput();
		#endif
		textInputEnabled = false;
	}
}

int upKey(){
	return keystates[ SDL_SCANCODE_UP ];
}

int downKey(){
	return keystates[ SDL_SCANCODE_DOWN ];
}

int leftKey(){
	return keystates[ SDL_SCANCODE_LEFT ];
}

int rightKey(){
	return keystates[ SDL_SCANCODE_RIGHT ];
}

int shiftKey(){
	return keystates[ SDL_SCANCODE_LSHIFT ]
		|| keystates[ SDL_SCANCODE_RSHIFT ];
}

int commandKey(){
	return keystates[ SDL_SCANCODE_LGUI ]
		|| keystates[ SDL_SCANCODE_RGUI ];
}

int controlKey(){
	return keystates[ SDL_SCANCODE_LCTRL ]
		|| keystates[ SDL_SCANCODE_RCTRL ];
}

int enterKey(){
	return keystates[ SDL_SCANCODE_RETURN ]
		|| keystates[ SDL_SCANCODE_KP_ENTER ];
}

int escapeKey(){
	return keystates[ SDL_SCANCODE_ESCAPE ];
}

int spaceKey(){
	return keystates[ SDL_SCANCODE_SPACE ];
}

int tabKey(){
	return keystates[ SDL_SCANCODE_TAB ];
}

int charKey( char key ){
	if( key >= 'A' && key <= 'Z' ) key = key - 'A' + 'a';
	if( key >= 'a' && key <= 'z' ){
		return keystates[ key - 'a' + SDL_SCANCODE_A ];
	}else if( key >= '0' && key <= '9' ){
		key = key == '0' ? 9 : key - '1';
		return keystates[ key + SDL_SCANCODE_1 ]
			|| keystates[ key + SDL_SCANCODE_KP_1 ];
	}
	return 0;
}

int upPad(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_DPAD_UP )
		: 0;
}

int downPad(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_DPAD_DOWN )
		: 0;
}

int leftPad(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_DPAD_LEFT )
		: 0;
}

int rightPad(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_DPAD_RIGHT )
		: 0;
}

int selectButton(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_BACK )
		: 0;
}

int startButton(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_START )
		: 0;
}

int aButton(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_A )
		: 0;
}

int bButton(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_B )
		: 0;
}

int xButton(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_X )
		: 0;
}

int yButton(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_Y )
		: 0;
}

int left1(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_LEFTSHOULDER )
		: 0;
}

int right1(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_RIGHTSHOULDER )
		: 0;
}

float left2(){
	return controller ?
		( SDL_GameControllerGetAxis( controller, SDL_CONTROLLER_AXIS_TRIGGERLEFT ) / 32767.0f )
		: 0.0f;
}

float right2(){
	return controller ?
		( SDL_GameControllerGetAxis( controller, SDL_CONTROLLER_AXIS_TRIGGERRIGHT ) / 32767.0f )
		: 0.0f;
}

int leftStick(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_LEFTSTICK )
		: 0;
}

int rightStick(){
	return controller ?
		SDL_GameControllerGetButton( controller, SDL_CONTROLLER_BUTTON_RIGHTSTICK )
		: 0;
}

float leftStickX(){
	return controller ?
		( SDL_GameControllerGetAxis( controller, SDL_CONTROLLER_AXIS_LEFTX ) / 32768.0f )
		: 0.0f;
}

float leftStickY(){
	return controller ?
		( SDL_GameControllerGetAxis( controller, SDL_CONTROLLER_AXIS_LEFTY ) / 32768.0f )
		: 0.0f;
}

float rightStickX(){
	return controller ?
		( SDL_GameControllerGetAxis( controller, SDL_CONTROLLER_AXIS_RIGHTX ) / 32768.0f )
		: 0.0f;
}

float rightStickY(){
	return controller ?
		( SDL_GameControllerGetAxis( controller, SDL_CONTROLLER_AXIS_RIGHTY ) / 32768.0f )
		: 0.0f;
}

int mouseButton( int button ){
	if( button == 2 )
		button = 3;
	else if( button == 3 )
		button = 2;
	return SDL_GetMouseState( nullptr, nullptr ) & SDL_BUTTON( button ) ? 1 : 0;
}

void showMouse( bool show ){
	SDL_ShowCursor( show ? SDL_ENABLE : SDL_DISABLE );
}

void trapMouse( bool trap ){
	if( SDL_SetRelativeMouseMode( trap ? SDL_TRUE : SDL_FALSE ) > -1 ){
		mouseTrapped = trap;
	}else{
		mouseTrapped = false;
	}
}

void hapticRumble( float strength, unsigned int length ){
	if( haptic )
		SDL_HapticRumblePlay( haptic, strength, (Uint32)length );
}

void end(){
	if( haptic ){
		SDL_HapticClose( haptic );
	}
	if( controller ){
		SDL_GameControllerClose( controller );
	}
	SDL_GL_DeleteContext( ctx );
	SDL_DestroyWindow( window );
	// TODO: Investigate why SDL_Quit segfaults in SDL 2.0.12.
	//SDL_Quit();
	exit( 0 );
}

#ifdef __STB_INCLUDE_STB_TRUETYPE_H__

void packFontRange( Font &font, int cpStart, int cpEnd ){
	if( !font.texture.success ) return;
	font.charStarts.push_back( cpStart );
	font.charEnds.push_back( cpEnd );
	int indexOffset = font.packedChars.size();
	int numChars = cpEnd - cpStart + 1;
	font.packedChars.resize( indexOffset + numChars );
	stbtt_PackFontRange(
		&font.pc,
		font.buffer.data(),
		0,
		STBTT_POINT_SIZE( font.size ),
		cpStart,
		numChars,
		&font.packedChars[ indexOffset ]
	);
	font.needSync = true;
}

int getCharacterIndex( int cp, Font &font ){
	int cpTotal = 0;
	for( size_t i = 0; i < font.charStarts.size(); i++ ){
		int cpStart = font.charStarts[ i ];
		int cpEnd = font.charEnds[ i ];
		if( (int)cp >= cpStart && (int)cp <= cpEnd )
			return cp - cpStart + cpTotal;
		cpTotal += cpEnd - cpStart + 1;
	}
	// dynamic packing
	packFontRange( font, cp, cp );
	return getCharacterIndex( cp, font );
}

Font loadFont( std::string fileName, float fontSize, int oversampleX, int oversampleY,
					bool prepack = true, int atlasWidth = 512, int atlasHeight = 512 ){
	Font font = newFont;
	
	FILE* file = fopen( fileName.c_str(), "rb" );
	if( !file ){
		fprintf( stderr, "Failed to open %s\n\n", fileName.c_str() );
		return font;
	}
	font.buffer = {};
	unsigned char buf[4096];
	while( size_t len = fread( buf, 1, sizeof( buf ), file ) ){
		std::vector<unsigned char> buf_vector( buf, buf + len );
		font.buffer.insert(
			font.buffer.end(),
			buf_vector.begin(),
			buf_vector.end()
		);
	}
	fclose( file );
	
	stbtt_InitFont( &font.info, font.buffer.data(), 0 );
	
	font.size = fontSize;
	
	// This may not be correct, but it works because Microsoft.
	int x0, y0, x1, y1;
	stbtt_GetFontBoundingBox( &font.info, &x0, &y0, &x1, &y1);
	font.height = (float)y1 * stbtt_ScaleForMappingEmToPixels( &font.info, font.size ) * 1.333f - 0.5f;
	
	font.atlas.resize( atlasWidth * atlasHeight );
	
	stbtt_PackBegin( &font.pc, font.atlas.data(), atlasWidth, atlasHeight, 0, 1, nullptr );
	stbtt_PackSetOversampling( &font.pc, oversampleX, oversampleY );
	
	font.texture = loadTexture( font.atlas.data(), atlasWidth, atlasHeight, 1, false, true );
	
	std::vector<Vertex> noVertices = {};
	std::vector<Index> noIndices = {};
	font.textMesh = loadMesh( noVertices, noIndices, true );
	
	// ASCII
	if( prepack ){
		packFontRange( font, 32, 126 );
		updateTexture( font.texture, font.atlas.data() );
	}
	
	return font;
}

float getTextWidthUtf32( std::u32string codepoints, Font &font ){
	Texture &tex = font.texture;
	if( !tex.success ) return 0.0f;
	float kernScale = stbtt_ScaleForPixelHeight( &font.info, font.height );
	// for stb_truetype's automatic positioning
	float charX = 0.0f, charY = 0.0f;
	for( size_t i = 0; i < codepoints.length(); i++ ){
		auto cp = codepoints[ i ];
		int ci = getCharacterIndex( cp, font );
		stbtt_aligned_quad q;
		// integer positioning needs to be disabled for kerning to work properly
		stbtt_GetPackedQuad( font.packedChars.data(), tex.width, tex.height, ci, &charX, &charY, &q, 0 );
		// set the kern offset for the next character
		if( codepoints.length() - i > 1 )
			charX += stbtt_GetCodepointKernAdvance( &font.info, cp, codepoints[ i + 1 ] ) * kernScale;
	}
	return charX;
}

float getTextWidth( std::string text, Font &font ){
	if( !font.texture.success ) return 0.0f;
	return getTextWidthUtf32( utf8ToUtf32( text ), font );
}

void drawTextUtf32( std::u32string codepoints, Font &font, float posX, float posY, float scale, int align, float wordWrap ){
	// align modes -- 0: left, 1: center, 2: right
	
	Texture &tex = font.texture;
	if( !tex.success ) return;
	
	float leading = font.size * 1.2f;
	size_t wordStart = 0;
	float wordStartX = 0.0f;
	float kernScale = stbtt_ScaleForPixelHeight( &font.info, font.height );
	
	// for stb_truetype's automatic positioning
	float charX = 0.0f, charY = 0.0f;
	
	std::vector<Vertex> vertices;
	std::vector<Index> indices;
	
	// buffer characters
	for( size_t i = 0; i < codepoints.length(); i++ ){
		auto cp = codepoints[ i ];
		// Get the font character index of codepoints >= 32 (space).
		// Don't print control characters.
		int ci = getCharacterIndex( cp < 32 ? 32 : cp, font );
		
		stbtt_aligned_quad q;
		// integer positioning needs to be disabled for kerning to work properly
		stbtt_GetPackedQuad( font.packedChars.data(), tex.width, tex.height, ci, &charX, &charY, &q, 0 );
		
		// set the kern offset for the next character
		if( codepoints.length() - i > 1 )
			charX += stbtt_GetCodepointKernAdvance( &font.info, cp, codepoints[ i + 1 ] ) * kernScale;
		
		// handle newlines and word wrapping
		if( cp == ' ' ){
			wordStart = i + 1;
			wordStartX = charX;
		}else if( cp == '\n' ){
			drawTextUtf32( codepoints.substr( i + 1 ), font, posX, posY + leading * scale, scale, align, wordWrap );
			break;
		}else if( wordWrap > 0.0f && charX * scale > wordWrap && wordStart > 0 ){
			// re-align
			charX = wordStartX;
			// delete the last characters
			vertices.resize( ( wordStart - 1 ) * 4 );
			indices.resize( ( wordStart - 1 ) * 6 );
			drawTextUtf32( codepoints.substr( wordStart ), font, posX, posY + leading * scale, scale, align, wordWrap );
			break;
		}
		
		// add the character to the vertex and index vectors
		Index idx = (Index)vertices.size();
		vertices.insert( vertices.end(), {
		  {	{ q.x0, -q.y0,  0.0f },
			{ 0.0f,  0.0f,  1.0f },
			{ 1.0f,  0.0f,  0.0f },
			{ q.s0,  q.t0 } },
		  {	{ q.x0, -q.y1,  0.0f },
			{ 0.0f,  0.0f,  1.0f },
			{ 1.0f,  0.0f,  0.0f },
			{ q.s0,  q.t1 } },
		  {	{ q.x1, -q.y1,  0.0f },
			{ 0.0f,  0.0f,  1.0f },
			{ 1.0f,  0.0f,  0.0f },
			{ q.s1,  q.t1 } },
		  {	{ q.x1, -q.y0,  0.0f },
			{ 0.0f,  0.0f,  1.0f },
			{ 1.0f,  0.0f,  0.0f },
			{ q.s1,  q.t0 } }
		} );
		indices.insert( indices.end(), {
			idx, idx + 1, idx + 2, idx + 2, idx + 3, idx
		} );
	}
	
	if( font.needSync ){
		updateTexture( tex, font.atlas.data() );
		font.needSync = false;
	}
	
	// copy old state
	linalg::mat<double,4,4> oldTexMatrix = texMatrix;
	
	double screenWidth = getDisplayWidth(), screenHeight = getDisplayHeight();
	texMatrix = linalg::identity;
	setTexture( tex, 0 );
	updateMesh( font.textMesh, vertices, indices, true );
	posX += charX * scale * ( align == 0 ? 0.0f : ( align == 1 ? -0.5f : -1.0f ) );
	posY += font.size * scale;
	drawMesh(
		font.textMesh,
		linalg::mul(
			linalg::translation_matrix( linalg::vec<double,3>(
				-1.0 + posX * 2.0 / screenWidth,
				1.0 - posY * 2.0 / screenHeight,
				0.0
			) ),
			linalg::scaling_matrix( linalg::vec<double,3>(
				2.0 / screenWidth * scale,
				2.0 / screenHeight * scale,
				1.0
			) )
		),
		linalg::identity,
		linalg::identity
	);
	
	// restore old state
	texMatrix = oldTexMatrix;
}

void drawText( std::string text, Font &font, float posX, float posY, float scale, int align = 0, float wordWrap = 0.0f ){
	// align modes -- 0: left, 1: center, 2: right
	
	if( !font.texture.success ) return;
	
	drawTextUtf32( utf8ToUtf32( text ), font, posX, posY, scale, align, wordWrap );
}

#else

void packFontRange( Font &font, int cpStart, int cpEnd ){
	return;
}

int getCharacterIndex( int cp, Font &font ){
	return 0;
}

Font loadFont( std::string fileName, float fontSize, int oversampleX, int oversampleY,
					bool prepack = true, int atlasWidth = 512, int atlasHeight = 512 ){
	fprintf( stderr, "Include stb_truetype.h before fg2.h to load TrueType fonts.\n\n" );
	return newFont;
}

float getTextWidthUtf32( std::u32string codepoints, Font &font ){
	return 0.0f;
}

float getTextWidth( std::string text, Font &font ){
	return 0.0f;
}

void drawTextUtf32( std::u32string codepoints, Font &font, float posX, float posY, float scale, int align = 0, float wordWrap = 0.0f ){
	return;
}

void drawText( std::string text, Font &font, float posX, float posY, float scale, int align = 0, float wordWrap = 0.0f ){
	return;
}

#endif // __STB_INCLUDE_STB_TRUETYPE_H__

} // namespace fg2

#endif // FG2_H


Mode Type Size Ref File
100644 blob 98 227abf3bfa53b2530dcc74495da7bd0ccdcb0775 .gitignore
100644 blob 225 9b00c2c2e7b4f0c1e338fdead65f17ba0af089c1 COPYING
100755 blob 43 45aea818a4a3202b2467509f28a481cce08834d2 Confectioner.command
100644 blob 14015 649b7f0c112c3ac13287bfe88b949fec50356e4d Makefile
100644 blob 2723 b5a3f573f076ef740ca742ec9598043732e10c0e README.md
040000 tree - 6b3a1677d07517c1f83769dd7675fe6bb9d7a269 base
100755 blob 156 84cb1387849f2ca98e53e43536d00af2dfabf7d3 caveconfec
100755 blob 28 41b0ef285892c86306eaa269f366dd04cb633d21 caveconfec.bat
100644 blob 198037 a0180394c9bf29c02b7ef05916bd5573e3f37da2 confec.cpp
100644 blob 487269 29cfd3578eb40b1f039e271bcaa81af49d1b7f3c gamecontrollerdb.txt
040000 tree - 62e9d686bbab52d3d88886390b437a3ecef315de include
100755 blob 12081 ad29f012941aedfd4ee7232ed95fb68c8c5244c9 index-template.html
100755 blob 1065 a460e3c74b8fa53a6f609944ef7e41558479e73f libs.cpp
100755 blob 27581 8350a63e947e8a4a55608fd090d128fef7b969a1 micropather.cpp
100644 blob 141235 f54e2d2631a628876a631456c043b77da5db78bd openjdk.pem
100755 blob 8 e9a74187b02a27b165dfa4f93bf6f060376d0ee6 steam_appid.txt
Hints:
Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://rocketgit.com/user/mse/ConfectionerEngine

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@ssh.rocketgit.com/user/mse/ConfectionerEngine

Clone this repository using git:
git clone git://git.rocketgit.com/user/mse/ConfectionerEngine

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a merge request:
... clone the repository ...
... make some changes and some commits ...
git push origin main