#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