Skip to content

Instantly share code, notes, and snippets.

@seanbaxter
Last active March 24, 2021 06:25
Show Gist options
  • Save seanbaxter/583d16b35dec99f4d6cb22e479a9b6c8 to your computer and use it in GitHub Desktop.
Save seanbaxter/583d16b35dec99f4d6cb22e479a9b6c8 to your computer and use it in GitHub Desktop.
Circle glTF viewer. C++ shaders.
#pragma once
// #include <imgui.h>
// #include <examples/imgui_impl_glfw.h>
// #include <examples/imgui_impl_opengl3.h>
#include <stb_image.h>
#include <gl3w/GL/gl3w.h>
#include <GLFW/glfw3.h>
#include <cmath>
#include <cstdio>
#include <cfloat>
#include <cstring>
template<typename type_t>
const char* enum_to_string(type_t e) {
static_assert(std::is_enum_v<type_t>);
switch(e) {
@meta for enum(type_t e2 : type_t) {
case e2:
return @enum_name(e2);
}
default:
return nullptr;
}
}
#define PRINT_ERROR() { \
auto err = glGetError(); \
if(err) {\
printf("%s:%d: %d\n", __FILE__, __LINE__, err); \
exit(1); \
} \
}
inline void print_matrix(const mat4& m) {
for(int row = 0; row < 4; ++row)
printf("%f %f %f %f\n", m[0][row], m[1][row], m[2][row], m[3][row]);
}
constexpr vec4 qmul(vec4 q, vec4 p) {
return vec4(
q.w * p.xyz + p.w * q.xyz + cross(q.xyz, p.xyz),
p.w * q.w - dot(p.xyz, q.xyz)
);
}
constexpr vec4 qinv(vec4 q) {
// Change the sign of the imaginary components.
q.xyz = -q.xyz;
return q;
}
// https://users.aalto.fi/~ssarkka/pub/quat.pdf
constexpr vec4 qlog(vec4 q) {
vec3 v = normalize(q.xzy);
return vec4(v * acosf(q.w), 0);
}
constexpr vec4 slerp(vec4 q0, vec4 q1, float t) {
float d = dot(q0, q1);
float theta_0 = acosf(d);
float theta = theta_0 * t;
vec4 q2 = normalize(q1 - q1 * d);
return q0 * cosf(theta) + q2 * sinf(theta);
}
inline mat4 make_perspective(float fov, float ar, float near, float far) {
float f = 1 / tan(fov / 2);
if(FLT_MAX == far) {
return mat4(
f / ar, 0, 0, 0,
0, f, 0, 0,
0, 0, -1, -1,
0, 0, -2 * near, 0
);
} else {
float range = near - far;
return mat4(
f / ar, 0, 0, 0,
0, f, 0, 0,
0, 0, (far + near) / range, -1,
0, 0, 2 * far * near / range, 0
);
}
}
inline mat4 make_lookat(vec3 eye, vec3 at, vec3 up) {
vec3 zaxis = normalize(eye - at);
vec3 xaxis = normalize(cross(up, zaxis));
vec3 yaxis = cross(zaxis, xaxis);
return mat4(
xaxis.x, yaxis.x, zaxis.x, 0,
xaxis.y, yaxis.y, zaxis.y, 0,
xaxis.z, yaxis.z, zaxis.z, 0,
-dot(xaxis, eye), -dot(yaxis, eye), -dot(zaxis, eye), 1
);
}
inline mat4 make_scale(vec3 scale) {
return mat4(
scale.x, 0, 0, 0,
0, scale.y, 0, 0,
0, 0, scale.z, 0,
0, 0, 0, 1
);
}
inline mat4 make_translate(vec3 translate) {
return mat4(
1, 0, 0, 0,
0, 1, 0, 0,
0, 0, 1, 0,
translate, 1
);
}
inline mat4 make_rotateX(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat4(
1, 0, 0, 0,
0, c, -s, 0,
0, s, c, 0,
0, 0, 0, 1
);
}
inline mat4 make_rotateY(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat4(
c, 0, -s, 0,
0, 1, 0, 0,
s, 0, c, 0,
0, 0, 0, 1
);
}
inline mat4 make_rotateZ(float angle) {
float s = sin(angle);
float c = cos(angle);
return mat4(
c, -s, 0, 0,
s, c, 0, 0,
0, 0, 1, 0,
0, 0, 0, 1
);
}
inline mat4 make_rotate(vec3 angles) {
mat4 x = make_rotateX(angles.x);
mat4 y = make_rotateY(angles.y);
mat4 z = make_rotateZ(angles.z);
return z * y * x;
}
////////////////////////////////////////////////////////////////////////////////
GLuint load_texture(const char* path, int& width, int& height) {
int comp;
stbi_uc* data = stbi_load(path, &width, &height, &comp, STBI_rgb_alpha );
printf("Loaded image %s %d %d %d\n", path, width, height, comp);
GLuint texture;
glCreateTextures(GL_TEXTURE_2D, 1, &texture);
glTextureStorage2D(texture, 1, GL_RGBA8, width, height);
glTextureSubImage2D(texture, 0, 0, 0, width, height, GL_RGBA,
GL_UNSIGNED_BYTE, data);
glGenerateTextureMipmap(texture);
// glTextureParameteri(image, GL_TEXTURE_WRAP_S, sampler.wrap_s);
// glTextureParameteri(image, GL_TEXTURE_WRAP_T, sampler.wrap_t);
glTextureParameteri(texture, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTextureParameteri(texture, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
return texture;
}
////////////////////////////////////////////////////////////////////////////////
template<typename type_t>
void specialize_shader(GLuint shader, const char* name, const type_t& obj) {
const int count = @member_count(type_t);
GLuint indices[count];
GLuint values[count] { };
@meta for(int i = 0; i < count; ++i) {
indices[i] = i;
memcpy(values + i, &@member_value(obj, i), sizeof(@member_type(type_t, i)));
}
glSpecializeShader(shader, name, count, indices, values);
}
////////////////////////////////////////////////////////////////////////////////
struct camera_t {
vec3 origin = vec3();
float pitch = 0;
float yaw = 0;
float log_distance = log(20.f);
// Perspective terms.
float fov = radians(20.f);
float near = .5;
float far = FLT_MAX;
void adjust(float pitch2, float yaw2, float d2);
vec3 get_eye() const noexcept;
mat4 get_view() const noexcept;
mat4 get_perspective(int width, int height) const noexcept;
mat4 get_xform(int width, int height) const noexcept;
};
inline void camera_t::adjust(float pitch2, float yaw2, float d2) {
pitch = clamp(pitch + pitch2, -radians(80.f), radians(80.f));
yaw = fmod(yaw + yaw2, 2 * M_PI);
log_distance += d2;
}
inline vec3 camera_t::get_eye() const noexcept {
float d = exp(log_distance);
return vec3(
sin(yaw) * cos(pitch) * d,
sin(pitch) * d,
cos(yaw) * cos(pitch) * d
);
}
inline mat4 camera_t::get_view() const noexcept {
return make_lookat(get_eye(), origin, vec3(0, 1, 0));
}
inline mat4 camera_t::get_perspective(int width, int height) const noexcept {
float ar = (float)width / height;
return make_perspective(fov, ar, near, far);
}
inline mat4 camera_t::get_xform(int width, int height) const noexcept {
return get_perspective(width, height) * get_view();
}
////////////////////////////////////////////////////////////////////////////////
class app_t {
public:
app_t(const char* name, int width = 800, int height = 600);
void loop();
virtual void display() { }
virtual void pos_callback(int xpos, int ypos) { }
virtual void size_callback(int width, int height) { }
virtual void close_callback() { }
virtual void refresh_callback() { }
virtual void focus_callback(int focused) { }
virtual void framebuffer_callback(int width, int height);
virtual void scale_callback(float xscale, float yscale) { }
virtual void cursor_callback(double xpos, double ypos);
virtual void button_callback(int button, int action, int mods);
virtual void debug_callback(GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length, const GLchar* message);
protected:
GLFWwindow* window = nullptr;
camera_t camera { };
int captured = false;
double last_x, last_y;
private:
static void _pos_callback(GLFWwindow* window, int xpos, int ypos);
static void _size_callback(GLFWwindow* window, int width, int height);
static void _close_callback(GLFWwindow* window);
static void _refresh_callback(GLFWwindow* window);
static void _focus_callback(GLFWwindow* window, int focused);
static void _framebuffer_callback(GLFWwindow* window, int width, int height);
static void _scale_callback(GLFWwindow* window, float xscale, float yscale);
static void _cursor_callback(GLFWwindow* window, double xpos, double ypos);
static void _button_callback(GLFWwindow* window, int button, int action,
int mods);
static void _debug_callback(GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length, const GLchar* message,
const void* user_param);
void register_callbacks();
};
app_t::app_t(const char* name, int width, int height) {
glfwWindowHint(GLFW_DOUBLEBUFFER, 1);
glfwWindowHint(GLFW_DEPTH_BITS, 24);
glfwWindowHint(GLFW_STENCIL_BITS, 8);
glfwWindowHint(GLFW_SAMPLES, 8); // HQ 4x multisample.
// glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6);
window = glfwCreateWindow(width, height, name, nullptr, nullptr);
glfwMakeContextCurrent(window);
glfwSwapInterval(1);
register_callbacks();
gl3wInit();
glEnable(GL_DEBUG_OUTPUT);
glEnable(GL_DEBUG_OUTPUT_SYNCHRONOUS);
glDebugMessageCallback(_debug_callback, this);
glfwGetWindowSize(window, &width, &height);
glViewport(0, 0, width, height);
// IMGUI_CHECKVERSION();
// ImGui::CreateContext();
// // ImGuiIO& io = ImGui::GetIO(); (void)io;
//
// ImGui_ImplGlfw_InitForOpenGL(window, true);
// ImGui_ImplOpenGL3_Init("#version 460");
}
void app_t::register_callbacks() {
glfwSetWindowUserPointer(window, this);
glfwSetWindowPosCallback(window, _pos_callback);
glfwSetWindowSizeCallback(window, _size_callback);
glfwSetWindowCloseCallback(window, _close_callback);
glfwSetWindowRefreshCallback(window, _refresh_callback);
glfwSetWindowFocusCallback(window, _focus_callback);
glfwSetFramebufferSizeCallback(window, _framebuffer_callback);
glfwSetCursorPosCallback(window, _cursor_callback);
glfwSetMouseButtonCallback(window, _button_callback);
// glfwSetWindowContentScaleCallback(window, _scale_callback);
}
void app_t::loop() {
while(!glfwWindowShouldClose(window)) {
display();
glfwSwapBuffers(window);
glfwPollEvents();
}
}
void app_t::framebuffer_callback(int width, int height) {
glViewport(0, 0, width, height);
}
void app_t::cursor_callback(double xpos, double ypos) {
if(captured) {
double dx = xpos - last_x;
double dy = ypos - last_y;
if(GLFW_PRESS == glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT)) {
camera.adjust(0, 0, dy / 100.f);
} else {
camera.adjust(-dy / 100.f, dx / 100.f, 0);
}
last_x = xpos;
last_y = ypos;
}
}
void app_t::button_callback(int button, int action, int mods) {
bool is_release =
GLFW_RELEASE == glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) &&
GLFW_RELEASE == glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_RIGHT);
if(!is_release && !captured) {
glfwGetCursorPos(window, &last_x, &last_y);
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
captured = true;
} else if(is_release && captured) {
glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
captured = false;
}
}
void app_t::debug_callback(GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length, const GLchar* message) {
printf("OpenGL: %s\n", message);
if(GL_DEBUG_SEVERITY_HIGH == severity ||
GL_DEBUG_SEVERITY_MEDIUM == severity)
exit(1);
}
void app_t::_pos_callback(GLFWwindow* window, int xpos, int ypos) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->pos_callback(xpos, ypos);
}
void app_t::_size_callback(GLFWwindow* window, int width, int height) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->size_callback(width, height);
}
void app_t::_close_callback(GLFWwindow* window) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->close_callback();
}
void app_t::_refresh_callback(GLFWwindow* window) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->refresh_callback();
}
void app_t::_focus_callback(GLFWwindow* window, int focused) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->focus_callback(focused);
}
void app_t::_framebuffer_callback(GLFWwindow* window, int width, int height) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->framebuffer_callback(width, height);
}
void app_t::_scale_callback(GLFWwindow* window, float xscale, float yscale) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->scale_callback(xscale, yscale);
}
void app_t::_cursor_callback(GLFWwindow* window, double xpos, double ypos) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->cursor_callback(xpos, ypos);
}
void app_t::_button_callback(GLFWwindow* window, int button, int action,
int mods) {
app_t* app = static_cast<app_t*>(glfwGetWindowUserPointer(window));
app->button_callback(button, action, mods);
}
void app_t::_debug_callback(GLenum source, GLenum type, GLuint id,
GLenum severity, GLsizei length, const GLchar* message,
const void* user_param) {
app_t* app = (app_t*)user_param;
app->debug_callback(source, type, id, severity, length, message);
}
////////////////////////////////////////////////////////////////////////////////
// Adapted from https://github.com/KhronosGroup/glTF-Sample-Viewer/blob/master/src/shaders/brdf.glsl
#pragma once
#include <cmath>
//
// Fresnel
//
// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
// https://github.com/wdas/brdf/tree/master/src/brdfs
// https://google.github.io/filament/Filament.md.html
inline vec3 F_None(vec3 f0, vec3 f90, float VdotH) {
return f0;
}
// The following equation models the Fresnel reflectance term of the spec equation (aka F())
// Implementation of fresnel from [4], Equation 15
inline vec3 F_Schlick(vec3 f0, vec3 f90, float VdotH) {
return f0 + (f90 - f0) * pow(clamp(1 - VdotH, 0.0f, 1.0f), 5);
}
inline vec3 F_CookTorrance(vec3 f0, vec3 f90, float VdotH) {
vec3 f0_sqrt = sqrt(f0);
vec3 ior = (1 + f0_sqrt) / (1 - f0_sqrt);
vec3 c = vec3(VdotH);
vec3 g = sqrt(sq(ior) + c*c - 1);
return 0.5f * pow(g-c, vec3(2)) /
pow(g+c, vec3(2)) * (1 + pow(c*(g+c) - 1, 2) / pow(c*(g-c) + 1, 2));
}
// Smith Joint GGX
// Note: Vis = G / (4 * NdotL * NdotV)
// see Eric Heitz. 2014. Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs. Journal of Computer Graphics Techniques, 3
// see Real-Time Rendering. Page 331 to 336.
// see https://google.github.io/filament/Filament.md.html#materialsystem/specularbrdf/geometricshadowing(specularg)
inline float V_GGX(float NdotL, float NdotV, float alphaRoughness) {
float r2 = sq(alphaRoughness);
float GGXV = NdotL * sqrt(NdotV * NdotV * (1 - r2) + r2);
float GGXL = NdotV * sqrt(NdotL * NdotL * (1 - r2) + r2);
float GGX = GGXV + GGXL;
return (GGX > 0) ? 0.5f / GGX : 0;
}
// Anisotropic GGX visibility function, with height correlation.
// T: Tanget, B: Bi-tanget
inline float V_GGX_anisotropic(float NdotL, float NdotV, float BdotV,
float TdotV, float TdotL, float BdotL, float anisotropy, float at, float ab) {
float GGXV = NdotL * length(vec3(at * TdotV, ab * BdotV, NdotV));
float GGXL = NdotV * length(vec3(at * TdotL, ab * BdotL, NdotL));
float v = 0.5f / (GGXV + GGXL);
return clamp(v, 0.f, 1.f);
}
// https://github.com/google/filament/blob/master/shaders/src/brdf.fs#L136
// https://github.com/google/filament/blob/master/libs/ibl/src/CubemapIBL.cpp#L179
// Note: Google call it V_Ashikhmin and V_Neubelt
inline float V_Ashikhmin(float NdotL, float NdotV) {
return clamp(1 / (4 * (NdotL + NdotV - NdotL * NdotV)), 0.f, 1.f);
}
// https://github.com/google/filament/blob/master/shaders/src/brdf.fs#L131
inline float V_Kelemen(float LdotH) {
// Kelemen 2001, "A Microfacet Based Coupled Specular-Matte BRDF Model with Importance Sampling"
return 0.25f / (LdotH * LdotH);
}
// The following equation(s) model the distribution of microfacet normals across the area being drawn (aka D())
// Implementation from "Average Irregularity Representation of a Roughened Surface for Ray Reflection" by T. S. Trowbridge, and K. P. Reitz
// Follows the distribution function recommended in the SIGGRAPH 2013 course notes from EPIC Games [1], Equation 3.
inline float D_GGX(float NdotH, float alphaRoughness) {
float alphaRoughnessSq = alphaRoughness * alphaRoughness;
float f = (NdotH * NdotH) * (alphaRoughnessSq - 1) + 1;
return alphaRoughnessSq / (M_PIf32 * f * f);
}
// Anisotropic GGX NDF with a single anisotropy parameter controlling the normal orientation.
// See https://google.github.io/filament/Filament.html#materialsystem/anisotropicmodel
// T: Tanget, B: Bi-tanget
inline float D_GGX_anisotropic(float NdotH, float TdotH, float BdotH,
float anisotropy, float at, float ab) {
float a2 = at * ab;
vec3 f = vec3(ab * TdotH, at * BdotH, a2 * NdotH);
float w2 = a2 / dot(f, f);
return a2 * w2 * w2 / M_PIf32;
}
inline float D_Ashikhmin(float NdotH, float alphaRoughness) {
// Ashikhmin 2007, "Distribution-based BRDFs"
float a2 = alphaRoughness * alphaRoughness;
float cos2h = NdotH * NdotH;
float sin2h = 1.0 - cos2h;
float sin4h = sin2h * sin2h;
float cot2 = -cos2h / (a2 * sin2h);
return 1 / (M_PIf32 * (4 * a2 + 1) * sin4h) * (4 * exp(cot2) + sin4h);
}
//Sheen implementation-------------------------------------------------------------------------------------
// See https://github.com/sebavan/glTF/tree/KHR_materials_sheen/extensions/2.0/Khronos/KHR_materials_sheen
// Estevez and Kulla http://www.aconty.com/pdf/s2017_pbs_imageworks_sheen.pdf
inline float D_Charlie(float sheenRoughness, float NdotH) {
sheenRoughness = max(sheenRoughness, 0.000001f); //clamp (0,1]
float alphaG = sheenRoughness * sheenRoughness;
float invR = 1 / alphaG;
float cos2h = NdotH * NdotH;
float sin2h = 1 - cos2h;
return (2 + invR) * pow(sin2h, invR * 0.5f) / (2 * M_PIf32);
}
//https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#acknowledgments AppendixB
inline vec3 BRDF_lambertian(vec3 f0, vec3 f90, vec3 diffuseColor, float VdotH) {
// see https://seblagarde.wordpress.com/2012/01/08/pi-or-not-to-pi-in-game-lighting-equation/
return (1 - F_Schlick(f0, f90, VdotH)) * (diffuseColor / M_PIf32);
}
// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#acknowledgments AppendixB
inline vec3 BRDF_specularGGX(vec3 f0, vec3 f90, float alphaRoughness,
float VdotH, float NdotL, float NdotV, float NdotH) {
vec3 F = F_Schlick(f0, f90, VdotH);
float Vis = V_GGX(NdotL, NdotV, alphaRoughness);
float D = D_GGX(NdotH, alphaRoughness);
return F * Vis * D;
}
inline vec3 BRDF_specularAnisotropicGGX(vec3 f0, vec3 f90,
float alphaRoughness, float VdotH, float NdotL, float NdotV, float NdotH,
float BdotV, float TdotV, float TdotL, float BdotL, float TdotH, float BdotH,
float anisotropy) {
// Roughness along tangent and bitangent.
// Christopher Kulla and Alejandro Conty. 2017. Revisiting Physically Based Shading at Imageworks
float at = max(alphaRoughness * (1 + anisotropy), 0.00001f);
float ab = max(alphaRoughness * (1 - anisotropy), 0.00001f);
vec3 F = F_Schlick(f0, f90, VdotH);
float V = V_GGX_anisotropic(NdotL, NdotV, BdotV, TdotV, TdotL, BdotL,
anisotropy, at, ab);
float D = D_GGX_anisotropic(NdotH, TdotH, BdotH, anisotropy, at, ab);
return F * V * D;
}
// f_sheen
vec3 BRDF_specularSheen(vec3 sheenColor, float sheenIntensity,
float sheenRoughness, float NdotL, float NdotV, float NdotH) {
float sheenDistribution = D_Charlie(sheenRoughness, NdotH);
float sheenVisibility = V_Ashikhmin(NdotL, NdotV);
return sheenColor * sheenIntensity * sheenDistribution * sheenVisibility;
}
#pragma once
const float GAMMA = 2.2;
const float INV_GAMMA = 1.0 / GAMMA;
// linear to sRGB approximation
// see http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html
inline vec3 linearTosRGB(vec3 color) {
return pow(color, vec3(INV_GAMMA));
}
// sRGB to linear approximation
// see http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html
inline vec3 sRGBToLinear(vec3 srgbIn) {
return vec3(pow(srgbIn.xyz, vec3(GAMMA)));
}
inline vec4 sRGBToLinear(vec4 srgbIn) {
return vec4(sRGBToLinear(srgbIn.xyz), srgbIn.w);
}
// Uncharted 2 tone map
// see: http://filmicworlds.com/blog/filmic-tonemapping-operators/
inline vec3 toneMapUncharted2Impl(vec3 color) {
const float A = 0.15;
const float B = 0.50;
const float C = 0.10;
const float D = 0.20;
const float E = 0.02;
const float F = 0.30;
return ((color*(A*color+C*B)+D*E)/(color*(A*color+B)+D*F))-E/F;
}
inline vec3 toneMapUncharted(vec3 color) {
const float W = 11.2;
color = toneMapUncharted2Impl(2 * color);
vec3 whiteScale = 1 / toneMapUncharted2Impl(W);
return linearTosRGB(color * whiteScale);
}
// Hejl Richard tone map
// see: http://filmicworlds.com/blog/filmic-tonemapping-operators/
inline vec3 toneMapHejlRichard(vec3 color) {
color = max(vec3(0.0), color - vec3(0.004));
return (color * (6.2f * color + .5f)) /
(color * (6.2f * color + 1.7f) + 0.06f);
}
// ACES tone map
// see: https://knarkowicz.wordpress.com/2016/01/06/aces-filmic-tone-mapping-curve/
inline vec3 toneMapACES(vec3 color) {
const float A = 2.51;
const float B = 0.03;
const float C = 2.43;
const float D = 0.59;
const float E = 0.14;
return linearTosRGB(clamp((color * (A * color + B)) / (color * (C * color + D) + E), 0.f, 1.f));
}
inline vec3 toneMap(vec3 color, float exposure) {
color *= exposure;
/*
#ifdef TONEMAP_UNCHARTED
return toneMapUncharted(color);
#endif
#ifdef TONEMAP_HEJLRICHARD
return toneMapHejlRichard(color);
#endif
#ifdef TONEMAP_ACES
return toneMapACES(color);
#endif
*/
return linearTosRGB(color);
}
#include <cgltf/cgltf.h>
#include <vector>
#include <iostream>
#include <type_traits>
#include <cassert>
#include <sys/stat.h>
#include <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include "appglfw.hxx"
#include "brdf.hxx"
#include "tonemapping.hxx"
// PBR metallic roughness
struct pbrMetallicRoughness_t {
// baseColorTexture
// metallicRoughnessTexture
vec4 baseColorFactor;
float metallicFactor;
float roughnessFactor;
};
// KHR_materials_pbrSpecularGlossiness
struct KHR_materials_pbrSpecularGlossiness_t {
// diffuseTexture
// specularGlossinessTexture
vec4 diffuseFactor;
vec3 specularFactor;
float glossinessFactor;
};
// KHR_materials_clearcoat
struct KHR_materials_clearcoat_t {
// clearcoatTexture
// clearcoatRoughnessTexture
// clearcoatNormalTexture
float clearcoatFactor;
float clearcoatRoughnessFactor;
};
// KHR_materials_transmission
struct KHR_materials_transmission_t {
// transmissionTexture
float transmissionFactor;
float ior;
};
struct material_uniform_t {
// Textures:
// normalTexture
// occlusionTexture
// emissiveTexture
vec3 emissiveFactor;
float alphaCutoff;
// Material models
pbrMetallicRoughness_t pbrMetallicRoughness;
KHR_materials_pbrSpecularGlossiness_t pbrSpecularGlossiness;
KHR_materials_clearcoat_t clearcoat;
KHR_materials_transmission_t transmission;
};
enum light_type_t {
light_type_directional,
light_type_point,
light_type_spot,
};
struct light_t {
vec3 direction;
float range;
vec3 color;
float intensity;
vec3 position;
float innerConeCos;
float outerConeCos;
light_type_t type;
vec2 padding;
};
struct uniform_t {
// Transform model space into world space.
mat4 model_to_world;
// Transform world space into clip space.
mat4 view_projection;
// The normal matrix is just a rotation (and possibly a scale). It
// ignores translation, so encode it as a mat3.
mat3 normal;
vec3 camera; // Position of camera.
float normal_scale;
material_uniform_t material;
float exposure;
int light_count;
light_t lights[16];
mat4 joint_matrices[75];
// TODO: Make these mat4x3. We only use 3D matrix operations.
mat4 joint_normal_matrices[75];
};
////////////////////////////////////////////////////////////////////////////////
// Vertex attribute and sampler binding locations.
enum vattrib_index_t {
// Shader locations for vertex attributes.
vattrib_position,
vattrib_normal,
vattrib_tangent,
vattrib_binormal,
// Repeat these vertex attributes in cycles of three. We don't have to
// bind all of them.
vattrib_texcoord0,
vattrib_joints0,
vattrib_weights0,
vattrib_texcoord1,
vattrib_joints1,
vattrib_weights1,
};
enum sampler_index_t {
// Core
sampler_normal,
sampler_occlusion,
sampler_emissive,
// pbrMetallicRoughness
sampler_baseColor,
sampler_metallicRoughness,
// pbrSpecularGlossiness
sampler_diffuse,
sampler_specularGlossiness,
// KHR_materials_clearcoat
sampler_clearcoat,
sampler_clearcoatRoughness,
sampler_clearcoatNormal,
// KHR_materials_transmission
sampler_transmission,
// Image-based lighting textures and LUTs.
// The Env samplers are cube maps.
sampler_GGXLut,
sampler_GGXEnv,
sampler_LambertianEnv,
sampler_CharlieLut,
sampler_CharlieEnv,
};
////////////////////////////////////////////////////////////////////////////////
// Vertex and fragment shader feature sets. The spaceship generates
// relational operators so we can use these as keys in maps.
// Shader specialization constants
struct vert_features_t {
// Each flag indicates the availability of a vertex attribute.
// vattrib_position is always available.
bool normal; // vattrib_normal
bool tangent; // vattrib_tangent + vattrib_binormal
bool texcoord0; // vattrib_texcoord0
bool texcoord1; // vattrib_texcoord1
bool joints0; // vattrib_joints0 + vattrib_weights0
bool joints1; // vattrib_joints1 + vattrib_weights1
};
[[spirv::constant(0)]]
vert_features_t vert_features;
struct frag_features_t {
// Incoming vertex properties.
bool normal;
bool tangents;
// Available texture maps.
bool normal_map;
bool emissive_map;
bool occlusion_map;
// Material properties.
bool metallicRoughness;
bool anisotropy;
bool ibl;
bool point_lights;
};
[[spirv::constant(0)]]
frag_features_t frag_features;
template<int I, typename type_t = vec4>
[[using spirv: in, location(I)]]
type_t shader_in;
template<int I, typename type_t = vec4>
[[using spirv: out, location(I)]]
type_t shader_out;
template<int I, typename type_t = sampler2D>
[[using spirv: uniform, binding(I)]]
type_t shader_sampler;
[[using spirv: uniform, binding(0)]]
uniform_t uniforms;
inline mat4 skinning_matrix(ivec4 joints, vec4 weights, const mat4* matrices) {
mat4 skin =
weights.x * matrices[joints.x] +
weights.y * matrices[joints.y] +
weights.z * matrices[joints.z] +
weights.w * matrices[joints.w];
return skin;
}
[[spirv::vert]]
void vert_main() {
// Always load the position attribute.
vec4 pos = shader_in<vattrib_position, vec4>;
// Apply skeletal animation.
/*
if(vert_features.joints0) {
// Compute the first 4 components of the skin matrix.
mat4 skin = skinning_matrix(
shader_in<vattrib_joints0, ivec4>,
shader_in<vattrib_weights0>,
uniforms.joint_matrices
);
if(vert_features.joints1) {
// Compute the next 4 components of the skin matrix.
skin += skinning_matrix(
shader_in<vattrib_joints1, ivec4>,
shader_in<vattrib_weights1>,
uniforms.joint_matrices
);
}
// Advance the position by the skin matrix.
pos = skin * pos;
}
*/
// Transform the model vertex into world space.
pos = uniforms.model_to_world * pos;
// Pass the vertex position to the fragment shader.
shader_out<vattrib_position, vec3> = pos.xyz / pos.w;
// Pass the vertex normal to the fragment shader.
if(vert_features.normal) {
// Load the vertex normal attribute.
vec3 n = shader_in<vattrib_normal, vec3>;
// Apply morphing.
// Apply skinning.
// Rotate into normal space and send to the fragment shader.
// TODO: Must we normalize? Is the normal vector already normalized coming
// out of the map?
shader_out<vattrib_normal, vec3> = normalize(uniforms.normal * n);
}
// Pass through texcoords.
if(vert_features.texcoord0)
shader_out<vattrib_texcoord0, vec2> = shader_in<vattrib_texcoord0, vec2>;
if(vert_features.texcoord1)
shader_out<vattrib_texcoord1, vec2> = shader_in<vattrib_texcoord1, vec2>;
// Set the vertex positions.
glvert_Output.Position = uniforms.view_projection * pos;
}
////////////////////////////////////////////////////////////////////////////////
struct normal_info_t {
vec3 ng;
vec3 n;
vec3 t;
vec3 b;
};
inline normal_info_t get_normal_info(vec3 pos, vec3 v, vec2 uv) {
// Get derivatives of texcoord wrt fragment coordinates.
vec3 uv_dx = vec3(glfrag_dFdx(uv), 0);
vec3 uv_dy = vec3(glfrag_dFdy(uv), 0);
vec3 t_ = (uv_dy.t * glfrag_dFdx(pos) - uv_dx.t * glfrag_dFdy(pos)) /
(uv_dx.s * uv_dy.t - uv_dy.s * uv_dx.t);
vec3 ng, t, b;
if(frag_features.tangents) {
ng = shader_in<vattrib_normal, vec3>;
t = shader_in<vattrib_tangent, vec3>;
b = shader_in<vattrib_binormal, vec3>;
} else {
if(frag_features.normal) {
ng = shader_in<vattrib_normal, vec3>;
} else {
ng = normalize(cross(glfrag_dFdx(pos), glfrag_dFdy(pos)));
}
// Compute tangent and binormal.
t = normalize(t_ - ng * dot(ng, t_));
b = cross(ng, t);
}
// For back-facing surface, the tangential basis vectors are negated.
float facing = 2 * step(0.f, dot(v, ng)) - 1;
t *= facing;
b *= facing;
ng *= facing;
vec3 direction;
if(frag_features.anisotropy) {
} else {
direction = vec3(1, 0, 0);
}
t = mat3(t, b, ng) * direction;
b = normalize(cross(ng, t));
vec3 n;
if(frag_features.normal_map) {
n = 2 * texture(shader_sampler<sampler_normal>, uv).rgb - 1;
n *= vec3(uniforms.normal_scale, uniforms.normal_scale, 1);
n = mat3(t, b, ng) * normalize(n);
} else {
n = ng;
}
normal_info_t info;
info.ng = ng;
info.t = t;
info.b = b;
info.n = n;
return info;
}
struct material_info_t {
vec3 f0;
float roughness;
vec3 albedo;
float alpha_roughness;
vec3 f90;
float metallic;
vec3 n;
vec3 base_color;
};
inline void get_metallic_roughness(material_info_t& info, float f0, vec2 uv) {
info.metallic = 1; uniforms.material.pbrMetallicRoughness.metallicFactor;
info.roughness = 1; uniforms.material.pbrMetallicRoughness.roughnessFactor;
// Sample the metallic-roughness texture. This has g and b channels.
vec4 mr = texture(shader_sampler<sampler_metallicRoughness>, uv);
info.roughness *= mr.g;
info.metallic *= mr.b;
// TODO: Provide for specular override of f0.
info.albedo = mix(info.base_color * (1 - f0), 0, info.metallic);
info.f0 = mix(f0, info.base_color, info.metallic);
}
// https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#appendix-b-brdf-implementation
inline float get_range_attenuation(float range, float distance) {
// NOTE: Multiple returns cause validation error.
if(range <= 0)
return 1;
else
return clamp(1.f - pow(distance / range, 4), 1.f, 0.f) /
(distance * distance);
}
inline float get_spot_attenuation(vec3 point_to_light, vec3 direction,
float outer_cos, float inner_cos) {
float cos = dot(normalize(direction), -normalize(point_to_light));
return
cos <= outer_cos ? 0 :
cos >= inner_cos ? 1 :
smoothstep(outer_cos, inner_cos, cos);
}
// Image-based lighting.
inline vec3 getIBLRadianceGGX(vec3 n, vec3 v, float roughness, vec3 color) {
int levels = textureQueryLevels(shader_sampler<sampler_GGXEnv, samplerCube>);
float NdotV = clamp(dot(n, v), 0.f, 1.f);
float lod = levels * clamp(roughness, 0.f, 1.f);
vec3 reflection = normalize(reflect(-v, n));
vec2 brdfSamplePoint = clamp(vec2(NdotV, roughness), 0, 1);
vec2 brdf = texture(
shader_sampler<sampler_GGXLut>,
brdfSamplePoint
).rg;
vec3 specular = textureLod(
shader_sampler<sampler_GGXEnv, samplerCube>,
reflection,
lod
).rgb;
return specular * (color * brdf.x + brdf.y);
}
inline vec3 getIBLRadianceLambertian(vec3 n, vec3 color) {
vec3 diffuseLight = texture(
shader_sampler<sampler_LambertianEnv, samplerCube>,
n
).rgb;
return diffuseLight * color;
}
[[spirv::frag]]
void frag_main() {
vec3 pos = shader_in<vattrib_position, vec3>;
vec2 uv = shader_in<vattrib_texcoord0, vec2>;
// TODO: Break into getBaseColor().
vec4 base_color = texture(shader_sampler<sampler_baseColor>, uv);
// if(frag_features.has_pbr_metallic_roughness) {
base_color = sRGBToLinear(base_color);
// }
vec3 v = normalize(uniforms.camera - pos);
// Get the position of the fragment.
normal_info_t normal = get_normal_info(pos, v, uv);
vec3 n = normal.n;
vec3 t = normal.t;
vec3 b = normal.b;
float NdotV = clamp(dot(n, v), 0.f, 1.f);
float TdotV = clamp(dot(t, v), 0.f, 1.f);
float BdotV = clamp(dot(b, v), 0.f, 1.f);
// The default index of refraction of 1.5 yields a dielectric normal incidence reflectance of 0.04.
float ior = 1.5;
float f0_ior = 0.04;
material_info_t material { };
material.base_color = base_color.rgb;
if(frag_features.metallicRoughness) {
// Load metallic-roughness properties once. We'll need these for each light
// in the scene.
get_metallic_roughness(material, f0_ior, uv);
}
// Clamp the metallic-roughness parameters.
material.roughness = clamp(material.roughness, 0.f, 1.f);
material.metallic = clamp(material.metallic, 0.f, 1.f);
material.alpha_roughness = sq(material.roughness);
// Compute reflectance.
float reflectance = max(max(material.f0.r, material.f0.g), material.f0.b);
material.f90 = vec3(clamp(50 * reflectance, 0.f, 1.f));
material.n = n;
vec3 f_diffuse(0);
vec3 f_specular(0);
if(frag_features.ibl) {
// Use image-based lighting.
f_specular += getIBLRadianceGGX(n, v, material.roughness, material.f0);
f_diffuse += getIBLRadianceLambertian(n, material.albedo);
}
// Specialization constant over point lighting.
if(frag_features.point_lights) {
// Dynamic uniform control flow over all lights.
for(int i = 0; i < uniforms.light_count; ++i) {
light_t light = uniforms.lights[i];
vec3 point_to_light = -light.direction;
float attenuation = 1;
if(light_type_directional == light.type) {
point_to_light = -light.direction;
} else if(light_type_point == light.type) {
point_to_light = light.position - pos;
attenuation = get_range_attenuation(light.range, length(point_to_light));
} else {
// light_type_spot
point_to_light = light.position - pos;
attenuation = get_range_attenuation(light.range, length(point_to_light));
attenuation *= get_spot_attenuation(point_to_light, light.direction,
light.outerConeCos, light.innerConeCos);
}
vec3 intensity = attenuation * light.intensity * light.color;
vec3 l = normalize(point_to_light);
vec3 h = normalize(l + v); // Half-angle vector.
float NdotL = clamp(dot(n, l), 0.f, 1.f);
float NdotH = clamp(dot(n, h), 0.f, 1.f);
float LdotH = clamp(dot(l, h), 0.f, 1.f);
float VdotH = clamp(dot(v, h), 0.f, 1.f);
if(NdotL > 0 || NdotV > 0) {
// Calculation of analytical light
//https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#acknowledgments AppendixB
f_diffuse += intensity * NdotL * BRDF_lambertian(material.f0,
material.f90, material.albedo, VdotH);
f_specular += intensity * NdotL * BRDF_specularGGX(material.f0,
material.f90, material.alpha_roughness, VdotH, NdotL, NdotV,
NdotH);
}
}
}
vec3 f_emissive(0);
if(frag_features.emissive_map) {
vec3 sample = texture(shader_sampler<sampler_emissive>, uv).rgb;
f_emissive = sRGBToLinear(sample);
}
vec3 color = f_diffuse + f_specular + f_emissive;
if(frag_features.occlusion_map) {
float ao = texture(shader_sampler<sampler_occlusion>, uv).r;
color = mix(color, color * ao, 1.f);
}
shader_out<0> = vec4(toneMap(color, uniforms.exposure), base_color.a);
}
////////////////////////////////////////////////////////////////////////////////
struct env_map_t {
// GL textures.
GLuint GGXLut; // specular/specular.ktx2
GLuint GGXEnv; // images/lut_ggx.png
GLuint LambertianEnv; // lambertian/diffuse.ktx2
GLuint CharlieLut; // images/lut_charlie.png
GLuint CharlieEnv; // charlie/sheen.ktx2
};
struct env_paths_t {
const char* GGXLut;
const char* GGXEnv;
const char* LambertianEnv;
const char* CharlieLut;
const char* CharlieEnv;
};
GLuint create_hdr_cubemap(const char* path) {
const uint8_t ktx2_version[12] {
0xAB, 0x4B, 0x54, 0x58, 0x20, 0x32, 0x30, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A
};
struct ktx2_header_t {
char identifier[12];
uint32_t vkFormat;
uint32_t typeSize;
uint32_t width;
uint32_t height;
uint32_t pixelDepth;
uint32_t layerCount;
uint32_t faceCount;
uint32_t levelCount;
uint32_t supercompressionScheme;
// Index
uint32_t dfdByteOffset;
uint32_t dfdByteLength;
uint32_t kvdByteOffset;
uint32_t kvdByteLength;
uint64_t sgdByteOffset;
uint64_t sgdByteLength;
struct indexing_t {
uint64_t byteOffset;
uint64_t byteLength;
uint64_t uncompressedByteLength;
};
indexing_t levels[1]; // levelCount elements.
};
int fd = open(path, O_RDONLY);
if(-1 == fd) {
printf("cannot open file %s\n", path);
exit(1);
}
struct stat statbuf;
if(-1 == fstat(fd, &statbuf)) {
printf("cannot stat file %s\n", path);
exit(1);
}
if(statbuf.st_size < sizeof(ktx2_header_t)) {
printf("file %s is too small to be a ktx2 file", path);
exit(1);
}
const char* data = (const char*)mmap(nullptr, statbuf.st_size, PROT_READ,
MAP_PRIVATE, fd, 0);
const ktx2_header_t& header = *(const ktx2_header_t*)data;
if(memcmp(header.identifier, ktx2_version, 12)) {
printf("file %s has the wrong KTX2 identifier", path);
exit(1);
}
if(6 != header.faceCount) {
printf("file %s does not hold a cube map", path);
exit(1);
}
GLenum format, iformat;
switch(header.vkFormat) {
case 97:
format = GL_HALF_FLOAT;
iformat = GL_RGBA16F;
break;
case 109:
format = GL_FLOAT;
iformat = GL_RGBA32F;
break;
default:
printf("cube map in %s is not encoded in a supported format", path);
exit(1);
}
// Construct the cube map and all of its face-layers.
GLuint cubemap;
glCreateTextures(GL_TEXTURE_CUBE_MAP, 1, &cubemap);
glTextureStorage2D(cubemap, header.levelCount, iformat, header.width,
header.height);
// The file is encoded by levels.
for(int level = 0; level < header.levelCount; ++level) {
ktx2_header_t::indexing_t indexing = header.levels[level];
// Find the mip map level dimensions.
uint32_t width = header.width >> level;
uint32_t height = header.height >> level;
// Upload all six levels at once.
const char* level_data = data + indexing.byteOffset;
glTextureSubImage3D(cubemap, level, 0, 0, 0, width, height, 6, GL_RGBA,
GL_HALF_FLOAT, level_data);
}
munmap((void*)data, statbuf.st_size);
close(fd);
return cubemap;
}
env_map_t load_env_map(env_paths_t paths) {
env_map_t map { };
int width, height;
map.GGXLut = load_texture(paths.GGXLut, width, height);
map.GGXEnv = create_hdr_cubemap(paths.GGXEnv);
map.LambertianEnv = create_hdr_cubemap(paths.LambertianEnv);
map.CharlieLut = load_texture(paths.CharlieLut, width, height);
map.CharlieEnv = create_hdr_cubemap(paths.CharlieEnv);
return map;
}
////////////////////////////////////////////////////////////////////////////////
int find_node_index(const cgltf_data* data, const cgltf_node* node) {
return node - data->nodes;
}
int find_image_index(const cgltf_data* data, cgltf_image* image) {
return image - data->images;
}
int find_sampler_index(const cgltf_data* data, cgltf_sampler* sampler) {
return sampler - data->samplers;
}
int find_texture_index(const cgltf_data* data, cgltf_texture* texture) {
return texture - data->textures;
}
int find_material_index(const cgltf_data* data, cgltf_material* material) {
return material - data->materials;
}
int find_view_index(const cgltf_data* data, const cgltf_buffer_view* view) {
return view - data->buffer_views;
}
int find_buffer_index(const cgltf_data* data, const cgltf_buffer* buffer) {
return buffer - data->buffers;
}
struct texture_view_t {
// Index of the texture in the gltf stream.
int index = -1;
// Index of the TEXCOORD_n in the vertex attribute stream.
int texcoord = 0;
// The scalar multiplied applied to each normal vector of the normal
// texture.
// Also strength for occlusionTextureInfo.
float scale = 1.f;
explicit operator bool() const noexcept { return -1 != index; }
};
struct material_textures_t {
// Core:
texture_view_t normal;
texture_view_t occlusion;
texture_view_t emissive;
// pbrMetallicRoughness
texture_view_t baseColor;
texture_view_t metallicRoughness;
// KHR_materials_pbrSpecularGlossiness
texture_view_t diffuse;
texture_view_t specularGlossiness;
// KHR_materials_clearcoat
texture_view_t clearcoat;
texture_view_t clearcoatRoughness;
texture_view_t clearcoatNormal;
// KHR_materials_transmission
texture_view_t transmission;
};
struct material_t {
bool has_pbr_metallic_roughness;
bool has_pbr_specular_glossiness;
bool has_clearcoat;
bool has_transmission;
material_uniform_t uniform;
material_textures_t textures;
};
// Binary search in the array of animation keyframe times.
// This searches the input accessor of the animation sampler.
std::pair<int, float> find_animation_interpolate(
const cgltf_animation_sampler* sampler, float t) {
// Check sampler->input->min and max.
cgltf_accessor* input = sampler->input;
assert(cgltf_component_type_r_32f == input->component_type);
assert(cgltf_type_scalar == input->type);
const cgltf_buffer_view* view = input->buffer_view;
const char* data = (const char*)view->buffer->data + view->offset;
const float* times = (const float*)data;
const float* lb = std::lower_bound(times, times + input->count, t);
int index = lb - times;
float weight = 0;
if(index < input->count) {
float t0 = times[index];
float t1 = times[index + 1];
weight = (t - t0) / (t1 - t0);
} else {
index = input->count - 1;
weight = 1;
}
return { index, weight };
}
vec3 interpolate_lerp(const cgltf_animation_sampler* sampler,
std::pair<int, float> interpolant) {
cgltf_accessor* output = sampler->output;
assert(cgltf_component_type_r_32f == output->component_type);
assert(cgltf_type_vec3 == output->type);
const cgltf_buffer_view* view = output->buffer_view;
const char* data = (const char*)view->buffer->data + view->offset;
const vec3* vectors = (const vec3*)data;
vec3 a = vectors[interpolant.first];
vec3 b = vectors[interpolant.first + 1];
return a * (1 - interpolant.second) + b * interpolant.second;
}
vec4 interpolate_slerp(const cgltf_animation_sampler* sampler,
std::pair<int, float> interpolant) {
cgltf_accessor* output = sampler->output;
assert(cgltf_component_type_r_32f == output->component_type);
assert(cgltf_type_vec4 == output->type);
const cgltf_buffer_view* view = output->buffer_view;
const char* data = (const char*)view->buffer->data + view->offset;
const vec4* quats = (const vec4*)data;
vec4 a = quats[interpolant.first];
vec4 b = quats[interpolant.first + 1];
return slerp(a, b, interpolant.second);
}
struct animation_t {
enum path_t {
path_translation,
path_rotation,
path_scale,
path_weights,
};
struct channel_t {
int sampler;
int node;
};
std::vector<channel_t> channels;
struct sampler_t {
};
};
struct sampler_t {
GLenum mag_filter, min_filter;
GLenum wrap_s, wrap_t;
};
struct texture_t {
int image;
int sampler;
};
struct prim_t {
int offset; // byte offset into the buffer.
int count;
vec3 min, max;
int material;
// The VAO for rendering the primitive.
GLuint vao = 0;
GLenum elements_type = GL_NONE;
};
struct mesh_t {
std::vector<prim_t> primitives;
};
// Create array buffers for storing vertex data.
struct model_t {
model_t(const char* path);
~model_t();
GLuint load_buffer(const cgltf_buffer* buffer);
GLuint load_image(const cgltf_image* image, const char* data_path);
sampler_t load_sampler(const cgltf_sampler* sampler);
texture_t load_texture(const cgltf_texture* texture);
material_t load_material(const cgltf_material* material);
texture_view_t load_texture_view(const cgltf_texture_view& view);
prim_t load_prim(const cgltf_primitive* prim);
mesh_t load_mesh(const cgltf_mesh* mesh);
void bind_texture(sampler_index_t sampler_index, texture_view_t view);
void bind_material(material_t& material);
void render_primitive(mesh_t& mesh, prim_t& prim);
std::vector<mesh_t> meshes;
std::vector<GLuint> buffers;
std::vector<GLuint> images;
std::vector<sampler_t> samplers;
std::vector<texture_t> textures;
std::vector<material_t> materials;
std::vector<light_t> lights;
cgltf_data* data = nullptr;
};
model_t::model_t(const char* path) {
cgltf_options options { };
cgltf_result result = cgltf_parse_file(&options, path, &data);
if(cgltf_result_success != result) {
std::cerr<< enum_to_string(result)<< "\n";
exit(1);
}
result = cgltf_load_buffers(&options, data, path);
if(cgltf_result_success != result) {
std::cerr<< enum_to_string(result)<< "\n";
exit(1);
}
// Load the buffers.
buffers.resize(data->buffers_count);
for(int i = 0; i < data->buffers_count; ++i) {
buffers[i] = load_buffer(data->buffers + i);
}
// Load the images.
images.resize(data->images_count);
for(int i = 0; i < data->images_count; ++i) {
images[i] = load_image(data->images + i, path);
}
// Load the textures.
textures.resize(data->textures_count);
for(int i = 0; i < data->textures_count; ++i) {
textures[i] = load_texture(data->textures + i);
}
// Load the samplers. These apply glTextureParameters to the textures.
samplers.resize(data->samplers_count);
for(int i = 0; i < data->samplers_count; ++i) {
samplers[i] = load_sampler(data->samplers + i);
}
// Load the materials.
materials.resize(data->materials_count);
for(int i = 0; i < data->materials_count; ++i) {
materials[i] = load_material(data->materials + i);
}
// Load the meshes.
for(int i = 0; i < data->meshes_count; ++i) {
meshes.push_back(load_mesh(data->meshes + i));
}
}
model_t::~model_t() {
for(mesh_t& mesh : meshes) {
for(prim_t& prim : mesh.primitives)
glDeleteVertexArrays(1, &prim.vao);
}
glDeleteBuffers(buffers.size(), buffers.data());
glDeleteTextures(images.size(), images.data());
cgltf_free(data);
}
GLuint model_t::load_buffer(const cgltf_buffer* buffer) {
GLuint buffer2;
glCreateBuffers(1, &buffer2);
glNamedBufferStorage(buffer2, buffer->size, buffer->data,
GL_DYNAMIC_STORAGE_BIT);
PRINT_ERROR();
printf("Loaded buffer with %zu bytes\n", buffer->size);
return buffer2;
}
GLuint model_t::load_image(const cgltf_image* image, const char* base) {
char path[260];
const char* s0 = strrchr(base, '/');
const char* s1 = strrchr(base, '\\');
const char* slash = s0 ? (s1 && s1 > s0 ? s1 : s0) : s1;
if(slash) {
size_t prefix = slash - base + 1;
strncpy(path, base, prefix);
strcpy(path + prefix, image->uri);
} else {
strcpy(path, image->uri);
}
int width, height;
return ::load_texture(path, width, height);
}
sampler_t model_t::load_sampler(const cgltf_sampler* sampler) {
sampler_t sampler2 { };
sampler2.mag_filter = sampler->mag_filter ? sampler->mag_filter :
GL_LINEAR;
sampler2.min_filter = sampler->min_filter ? sampler->min_filter :
GL_LINEAR_MIPMAP_LINEAR;
sampler2.wrap_s = sampler->wrap_s;
sampler2.wrap_t = sampler->wrap_t;
return sampler2;
}
texture_t model_t::load_texture(const cgltf_texture* texture) {
return {
find_image_index(data, texture->image),
find_sampler_index(data, texture->sampler)
};
}
material_t model_t::load_material(const cgltf_material* material) {
material_t material2 { };
// Core terms.
material2.uniform.emissiveFactor = vec3(
material->emissive_factor[0],
material->emissive_factor[1],
material->emissive_factor[2]
);
material2.uniform.alphaCutoff = material->alpha_cutoff;
material2.textures.normal = load_texture_view(material->normal_texture);
material2.textures.occlusion = load_texture_view(material->occlusion_texture);
material2.textures.emissive = load_texture_view(material->emissive_texture);
if(material->has_pbr_metallic_roughness) {
material2.has_pbr_metallic_roughness = true;
material2.uniform.pbrMetallicRoughness.baseColorFactor = vec4(
material->pbr_metallic_roughness.base_color_factor[0],
material->pbr_metallic_roughness.base_color_factor[1],
material->pbr_metallic_roughness.base_color_factor[2],
material->pbr_metallic_roughness.base_color_factor[3]
);
material2.uniform.pbrMetallicRoughness.metallicFactor =
material->pbr_metallic_roughness.metallic_factor;
material2.uniform.pbrMetallicRoughness.roughnessFactor =
material->pbr_metallic_roughness.roughness_factor;
material2.textures.baseColor = load_texture_view(
material->pbr_metallic_roughness.base_color_texture);
material2.textures.metallicRoughness = load_texture_view(
material->pbr_metallic_roughness.metallic_roughness_texture
);
}
if(material->has_pbr_specular_glossiness) {
material2.has_pbr_specular_glossiness = true;
material2.uniform.pbrSpecularGlossiness.diffuseFactor = vec4(
material->pbr_specular_glossiness.diffuse_factor[0],
material->pbr_specular_glossiness.diffuse_factor[1],
material->pbr_specular_glossiness.diffuse_factor[2],
material->pbr_specular_glossiness.diffuse_factor[3]
);
material2.uniform.pbrSpecularGlossiness.specularFactor = vec3(
material->pbr_specular_glossiness.specular_factor[0],
material->pbr_specular_glossiness.specular_factor[1],
material->pbr_specular_glossiness.specular_factor[2]
);
material2.uniform.pbrSpecularGlossiness.glossinessFactor =
material->pbr_specular_glossiness.glossiness_factor;
material2.textures.diffuse = load_texture_view(
material->pbr_specular_glossiness.diffuse_texture
);
material2.textures.specularGlossiness = load_texture_view(
material->pbr_specular_glossiness.specular_glossiness_texture
);
}
if(material->has_clearcoat) {
material2.has_clearcoat = true;
material2.uniform.clearcoat.clearcoatFactor =
material->clearcoat.clearcoat_factor;
material2.uniform.clearcoat.clearcoatRoughnessFactor =
material->clearcoat.clearcoat_roughness_factor;
material2.textures.clearcoat = load_texture_view(
material->clearcoat.clearcoat_texture
);
material2.textures.clearcoatRoughness = load_texture_view(
material->clearcoat.clearcoat_roughness_texture
);
material2.textures.clearcoatNormal = load_texture_view(
material->clearcoat.clearcoat_normal_texture
);
}
if(material->has_transmission) {
material2.has_transmission = true;
material2.uniform.transmission.transmissionFactor =
material->transmission.transmission_factor;
material2.textures.transmission = load_texture_view(
material->transmission.transmission_texture
);
}
return material2;
}
texture_view_t model_t::load_texture_view(const cgltf_texture_view& view) {
texture_view_t view2;
view2.index = find_texture_index(data, view.texture);
view2.texcoord = view2.texcoord;
view2.scale = view2.scale;
return view2;
}
prim_t model_t::load_prim(const cgltf_primitive* prim) {
prim_t prim2 { };
prim2.material = find_material_index(data, prim->material);
glCreateVertexArrays(1, &prim2.vao);
if(prim->indices) {
// Bind the index array.
const cgltf_accessor* accessor = prim->indices;
const cgltf_buffer_view* view = accessor->buffer_view;
prim2.offset = accessor->offset + view->offset;
prim2.count = accessor->count;
switch(accessor->component_type) {
case cgltf_component_type_r_8:
case cgltf_component_type_r_8u:
prim2.elements_type = GL_UNSIGNED_BYTE;
break;
case cgltf_component_type_r_16:
case cgltf_component_type_r_16u:
prim2.elements_type = GL_UNSIGNED_SHORT;
break;
case cgltf_component_type_r_32u:
prim2.elements_type = GL_UNSIGNED_INT;
break;
default:
break;
}
// Associate the buffer holding the indices.
GLuint buffer = buffers[find_buffer_index(data, view->buffer)];
glVertexArrayElementBuffer(prim2.vao, buffer);
}
for(int a = 0; a < prim->attributes_count; ++a) {
const cgltf_attribute* attrib = prim->attributes + a;
const cgltf_accessor* accessor = attrib->data;
const cgltf_buffer_view* view = accessor->buffer_view;
// Get the attribute location.
vattrib_index_t attribindex;
switch(attrib->type) {
case cgltf_attribute_type_position:
attribindex = vattrib_position;
break;
case cgltf_attribute_type_normal:
attribindex = vattrib_normal;
break;
case cgltf_attribute_type_texcoord:
attribindex = (vattrib_index_t)(vattrib_texcoord0 + 3 * attrib->index);
break;
case cgltf_attribute_type_joints:
attribindex = (vattrib_index_t)(vattrib_joints0 + 3 * attrib->index);
break;
case cgltf_attribute_type_weights:
attribindex = (vattrib_index_t)(vattrib_weights0 + 3 * attrib->index);
break;
}
// Use one binding per attribute.
GLuint buffer = buffers[find_buffer_index(data, view->buffer)];
glVertexArrayVertexBuffer(prim2.vao, attribindex, buffer,
view->offset + accessor->offset, accessor->stride);
// Enable the vertex attribute location.
glEnableVertexArrayAttrib(prim2.vao, attribindex);
// Get the attribute size and type.
GLenum type = GL_NONE;
int size = 0;
switch(accessor->type) {
case cgltf_type_scalar: size = 1; break;
case cgltf_type_vec2: size = 2; break;
case cgltf_type_vec3: size = 3; break;
case cgltf_type_vec4: size = 4; break;
default: break;
}
switch(accessor->component_type) {
case cgltf_component_type_r_8: type = GL_BYTE; break;
case cgltf_component_type_r_8u: type = GL_UNSIGNED_BYTE; break;
case cgltf_component_type_r_16: type = GL_SHORT; break;
case cgltf_component_type_r_16u: type = GL_UNSIGNED_SHORT; break;
case cgltf_component_type_r_32u: type = GL_UNSIGNED_INT; break;
case cgltf_component_type_r_32f: type = GL_FLOAT; break;
default: break;
}
// Associate the buffer view with the attribute location.
glVertexArrayAttribBinding(prim2.vao, attribindex, attribindex);
if(accessor->normalized || GL_FLOAT == type) {
glVertexArrayAttribFormat(prim2.vao, attribindex, size, type,
accessor->normalized, 0);
} else {
glVertexArrayAttribIFormat(prim2.vao, attribindex, size, type, 0);
}
}
return prim2;
}
mesh_t model_t::load_mesh(const cgltf_mesh* mesh) {
mesh_t mesh2;
mesh2.primitives.resize(mesh->primitives_count);
for(int i = 0; i < mesh->primitives_count; ++i)
mesh2.primitives[i] = load_prim(mesh->primitives + i);
return mesh2;
}
void model_t::bind_texture(sampler_index_t sampler_index,
texture_view_t view) {
texture_t texture = textures[view.index];
GLuint image = images[texture.image];
sampler_t sampler = samplers[texture.sampler];
glTextureParameteri(image, GL_TEXTURE_WRAP_S, sampler.wrap_s);
glTextureParameteri(image, GL_TEXTURE_WRAP_T, sampler.wrap_t);
glTextureParameteri(image, GL_TEXTURE_MIN_FILTER, sampler.min_filter);
glTextureParameteri(image, GL_TEXTURE_MAG_FILTER, sampler.mag_filter);
static float aniso = 0;
if(!aniso) {
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &aniso);
printf("aniso = %f\n", aniso);
}
glTextureParameteri(image, GL_TEXTURE_MAX_ANISOTROPY, aniso);
glBindTextureUnit(sampler_index, image);
}
void model_t::bind_material(material_t& mat) {
material_textures_t& tex = mat.textures;
if(tex.normal)
bind_texture(sampler_normal, tex.normal);
if(tex.occlusion)
bind_texture(sampler_occlusion, tex.occlusion);
if(tex.emissive)
bind_texture(sampler_emissive, tex.emissive);
if(tex.baseColor)
bind_texture(sampler_baseColor, tex.baseColor);
if(tex.metallicRoughness)
bind_texture(sampler_metallicRoughness, tex.metallicRoughness);
if(tex.diffuse)
bind_texture(sampler_diffuse, tex.diffuse);
if(tex.specularGlossiness)
bind_texture(sampler_specularGlossiness, tex.specularGlossiness);
if(tex.clearcoat)
bind_texture(sampler_clearcoat, tex.clearcoat);
if(tex.clearcoatRoughness)
bind_texture(sampler_clearcoatRoughness, tex.clearcoatRoughness);
if(tex.clearcoatNormal)
bind_texture(sampler_clearcoatNormal, tex.clearcoatNormal);
if(tex.transmission)
bind_texture(sampler_transmission, tex.transmission);
}
void model_t::render_primitive(mesh_t& mesh, prim_t& prim) {
bind_material(materials[prim.material]);
glBindVertexArray(prim.vao);
glDrawElements(GL_TRIANGLES, prim.count, prim.elements_type,
(void*)prim.offset);
}
////////////////////////////////////////////////////////////////////////////////
struct myapp_t : app_t {
myapp_t(const char* gltf_path, env_paths_t env_paths);
void display() override;
model_t model;
env_map_t env_map;
GLuint vs, fs, program;
uniform_t uniforms;
GLuint ubo;
};
myapp_t::myapp_t(const char* gltf_path, env_paths_t env_paths) :
app_t("glTF viewer", 800, 600),
model(gltf_path) {
// Load the environment maps.
env_map = load_env_map(env_paths);
// Compile the shaders.
vs = glCreateShader(GL_VERTEX_SHADER);
fs = glCreateShader(GL_FRAGMENT_SHADER);
GLuint shaders[] { vs, fs };
glShaderBinary(2, shaders, GL_SHADER_BINARY_FORMAT_SPIR_V_ARB,
__spirv_data, __spirv_size);
vert_features_t vert_features { };
vert_features.normal = true;
vert_features.texcoord0 = true;
specialize_shader(vs, @spirv(vert_main), vert_features);
frag_features_t frag_features { };
frag_features.normal = true;
frag_features.normal_map = true;
frag_features.emissive_map = true;
frag_features.occlusion_map = true;
frag_features.metallicRoughness = true;
frag_features.ibl = true;
frag_features.point_lights = false;
specialize_shader(fs, @spirv(frag_main), frag_features);
program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
// Load the uniforms.
glCreateBuffers(1, &ubo);
glNamedBufferStorage(ubo, sizeof(uniform_t), nullptr,
GL_DYNAMIC_STORAGE_BIT);
}
void myapp_t::display() {
const float bg[4] { 0 };
glClearBufferfv(GL_COLOR, 0, bg);
glClear(GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
glEnable(GL_CULL_FACE);
glFrontFace(GL_CCW);
glUseProgram(program);
int width, height;
glfwGetWindowSize(window, &width, &height);
mat4 view = camera.get_view();
mat4 projection = camera.get_perspective(width, height);
double int_part;
double speed = 10;
double angle = modf(glfwGetTime() / speed, &int_part) * (2 * M_PI);
mat4 model_to_world = make_rotateX(radians(-90.f));
model_to_world = make_rotateY(angle) * model_to_world;
uniforms.model_to_world = model_to_world;
uniforms.view_projection = projection * view;
uniforms.normal = mat3(uniforms.model_to_world);
uniforms.camera = camera.get_eye();
uniforms.normal_scale = 1;
uniforms.light_count = 0;
// TODO: Add a number of lights on random paths.
light_t light { };
// light.direction = vec3(-.7399, -.6428, -.1983);
light.position = 2 * vec3(0, sin(angle), cos(angle));
light.range = -1;
light.color = vec3(1, 1, 1);
light.intensity = 1;
light.innerConeCos = 0;
light.outerConeCos = cos(radians(45.f));
light.type = light_type_point;
uniforms.light_count = 2;
uniforms.lights[0] = light;
// Bind the environment maps.
glBindTextureUnit(sampler_GGXLut, env_map.GGXLut);
glBindTextureUnit(sampler_GGXEnv, env_map.GGXEnv);
glBindTextureUnit(sampler_LambertianEnv, env_map.LambertianEnv);
glBindTextureUnit(sampler_CharlieLut, env_map.CharlieLut);
glBindTextureUnit(sampler_CharlieEnv, env_map.CharlieEnv);
for(mesh_t& mesh : model.meshes) {
for(prim_t& prim : mesh.primitives) {
// Set the material for this primitive.
uniforms.material = model.materials[prim.material].uniform;
uniforms.exposure = 1;
glNamedBufferSubData(ubo, 0, sizeof(uniform_t), &uniforms);
glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo);
model.render_primitive(mesh, prim);
}
}
/*
// start the Dear ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplGlfw_NewFrame();
ImGui::NewFrame();
static bool show;
ImGui::ShowDemoWindow(&show);
ImGui::EndFrame();
ImGui::Render();
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
*/
}
int main(int argc, char** argv) {
glfwInit();
const char* path = (2 == argc) ?
argv[1] :
"/home/sean/projects/cgltf/test/glTF-Sample-Models/2.0/DamagedHelmet/glTF/DamagedHelmet.gltf";
env_paths_t env_paths {
"/home/sean/projects/glTF-Sample-Viewer/assets/images/lut_ggx.png",
"/home/sean/projects/glTF-Sample-Viewer/assets/environments/helipad/ggx/specular.ktx2",
"/home/sean/projects/glTF-Sample-Viewer/assets/environments/helipad/lambertian/diffuse.ktx2",
"/home/sean/projects/glTF-Sample-Viewer/assets/images/lut_charlie.png",
"/home/sean/projects/glTF-Sample-Viewer/assets/environments/helipad/charlie/sheen.ktx2",
};
myapp_t myapp(path, env_paths);
myapp.loop();
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment