This commit is contained in:
zack 2025-05-23 21:13:53 -04:00
commit 444f800536
No known key found for this signature in database
GPG key ID: EE8A2B709E2401D1
122 changed files with 17137 additions and 0 deletions

121
shared/Bitmap.h Normal file
View file

@ -0,0 +1,121 @@
#pragma once
#include <string.h>
#include <vector>
#include <glm/glm.hpp>
enum eBitmapType
{
eBitmapType_2D,
eBitmapType_Cube
};
enum eBitmapFormat
{
eBitmapFormat_UnsignedByte,
eBitmapFormat_Float,
};
/// R/RG/RGB/RGBA bitmaps
struct Bitmap
{
Bitmap() = default;
Bitmap(int w, int h, int comp, eBitmapFormat fmt)
:w_(w), h_(h), comp_(comp), fmt_(fmt), data_(w * h * comp * getBytesPerComponent(fmt))
{
initGetSetFuncs();
}
Bitmap(int w, int h, int d, int comp, eBitmapFormat fmt)
:w_(w), h_(h), d_(d), comp_(comp), fmt_(fmt), data_(w * h * d * comp * getBytesPerComponent(fmt))
{
initGetSetFuncs();
}
Bitmap(int w, int h, int comp, eBitmapFormat fmt, const void* ptr)
:w_(w), h_(h), comp_(comp), fmt_(fmt), data_(w * h * comp * getBytesPerComponent(fmt))
{
initGetSetFuncs();
memcpy(data_.data(), ptr, data_.size());
}
int w_ = 0;
int h_ = 0;
int d_ = 1;
int comp_ = 3;
eBitmapFormat fmt_ = eBitmapFormat_UnsignedByte;
eBitmapType type_ = eBitmapType_2D;
std::vector<uint8_t> data_;
static int getBytesPerComponent(eBitmapFormat fmt)
{
if (fmt == eBitmapFormat_UnsignedByte) return 1;
if (fmt == eBitmapFormat_Float) return 4;
return 0;
}
void setPixel(int x, int y, const glm::vec4& c)
{
(*this.*setPixelFunc)(x, y, c);
}
glm::vec4 getPixel(int x, int y) const
{
return ((*this.*getPixelFunc)(x, y));
}
private:
using setPixel_t = void(Bitmap::*)(int, int, const glm::vec4&);
using getPixel_t = glm::vec4(Bitmap::*)(int, int) const;
setPixel_t setPixelFunc = &Bitmap::setPixelUnsignedByte;
getPixel_t getPixelFunc = &Bitmap::getPixelUnsignedByte;
void initGetSetFuncs()
{
switch (fmt_)
{
case eBitmapFormat_UnsignedByte:
setPixelFunc = &Bitmap::setPixelUnsignedByte;
getPixelFunc = &Bitmap::getPixelUnsignedByte;
break;
case eBitmapFormat_Float:
setPixelFunc = &Bitmap::setPixelFloat;
getPixelFunc = &Bitmap::getPixelFloat;
break;
}
}
void setPixelFloat(int x, int y, const glm::vec4& c)
{
const int ofs = comp_ * (y * w_ + x);
float* data = reinterpret_cast<float*>(data_.data());
if (comp_ > 0) data[ofs + 0] = c.x;
if (comp_ > 1) data[ofs + 1] = c.y;
if (comp_ > 2) data[ofs + 2] = c.z;
if (comp_ > 3) data[ofs + 3] = c.w;
}
glm::vec4 getPixelFloat(int x, int y) const
{
const int ofs = comp_ * (y * w_ + x);
const float* data = reinterpret_cast<const float*>(data_.data());
return glm::vec4(
comp_ > 0 ? data[ofs + 0] : 0.0f,
comp_ > 1 ? data[ofs + 1] : 0.0f,
comp_ > 2 ? data[ofs + 2] : 0.0f,
comp_ > 3 ? data[ofs + 3] : 0.0f);
}
void setPixelUnsignedByte(int x, int y, const glm::vec4& c)
{
const int ofs = comp_ * (y * w_ + x);
if (comp_ > 0) data_[ofs + 0] = uint8_t(c.x * 255.0f);
if (comp_ > 1) data_[ofs + 1] = uint8_t(c.y * 255.0f);
if (comp_ > 2) data_[ofs + 2] = uint8_t(c.z * 255.0f);
if (comp_ > 3) data_[ofs + 3] = uint8_t(c.w * 255.0f);
}
glm::vec4 getPixelUnsignedByte(int x, int y) const
{
const int ofs = comp_ * (y * w_ + x);
return glm::vec4(
comp_ > 0 ? float(data_[ofs + 0]) / 255.0f : 0.0f,
comp_ > 1 ? float(data_[ofs + 1]) / 255.0f : 0.0f,
comp_ > 2 ? float(data_[ofs + 2]) / 255.0f : 0.0f,
comp_ > 3 ? float(data_[ofs + 3]) / 255.0f : 0.0f);
}
};

33
shared/CMakeLists.txt Normal file
View file

@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.19)
project(SharedUtils CXX C)
include(../CMake/CommonMacros.txt)
file(GLOB_RECURSE SRC_FILES LIST_DIRECTORIES false RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.c??)
file(GLOB_RECURSE HEADER_FILES LIST_DIRECTORIES false RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} *.h)
add_library(SharedUtils ${SRC_FILES} ${HEADER_FILES})
set_property(TARGET SharedUtils PROPERTY CXX_STANDARD 20)
set_property(TARGET SharedUtils PROPERTY CXX_STANDARD_REQUIRED ON)
# Add include directory for the root of the project to fix include paths
target_include_directories(SharedUtils PUBLIC ${CMAKE_SOURCE_DIR})
target_link_libraries(SharedUtils PUBLIC assimp)
target_link_libraries(SharedUtils PUBLIC glslang)
target_link_libraries(SharedUtils PUBLIC glfw)
target_link_libraries(SharedUtils PUBLIC LUtils)
target_link_libraries(SharedUtils PUBLIC LVKLibrary)
target_link_libraries(SharedUtils PUBLIC LVKstb)
target_link_libraries(SharedUtils PUBLIC ktx)
if(WIN32)
target_compile_definitions(SharedUtils PUBLIC "NOMINMAX")
endif()
if(UNIX AND NOT APPLE)
target_link_libraries(SharedUtils PUBLIC tbb)
endif()

232
shared/Camera.h Normal file
View file

@ -0,0 +1,232 @@
#pragma once
#include <assert.h>
#include <algorithm>
#include "shared/UtilsMath.h"
#include "shared/Trackball.h"
#include "glm/gtx/euler_angles.hpp"
class CameraPositionerInterface
{
public:
virtual ~CameraPositionerInterface() = default;
virtual glm::mat4 getViewMatrix() const = 0;
virtual glm::vec3 getPosition() const = 0;
};
class Camera final
{
public:
explicit Camera(CameraPositionerInterface& positioner)
: positioner_(&positioner)
{}
Camera(const Camera&) = default;
Camera& operator = (const Camera&) = default;
glm::mat4 getViewMatrix() const { return positioner_->getViewMatrix(); }
glm::vec3 getPosition() const { return positioner_->getPosition(); }
glm::mat4 getProjMatrix() const { return proj_; }
private:
const CameraPositionerInterface* positioner_;
glm::mat4 proj_;
};
class CameraPositioner_FirstPerson final: public CameraPositionerInterface
{
public:
CameraPositioner_FirstPerson() = default;
CameraPositioner_FirstPerson(const glm::vec3& pos, const glm::vec3& target, const glm::vec3& up)
: cameraPosition_(pos)
, cameraOrientation_(glm::lookAt(pos, target, up))
, up_(up)
{}
void update(double deltaSeconds, const glm::vec2& mousePos, bool mousePressed)
{
if (mousePressed)
{
const glm::vec2 delta = mousePos - mousePos_;
const glm::quat deltaQuat = glm::quat(glm::vec3(-mouseSpeed_ * delta.y, mouseSpeed_ * delta.x, 0.0f));
cameraOrientation_ = deltaQuat * cameraOrientation_;
cameraOrientation_ = glm::normalize(cameraOrientation_);
setUpVector(up_);
}
mousePos_ = mousePos;
const glm::mat4 v = glm::mat4_cast(cameraOrientation_);
const glm::vec3 forward = -glm::vec3(v[0][2], v[1][2], v[2][2]);
const glm::vec3 right = glm::vec3(v[0][0], v[1][0], v[2][0]);
const glm::vec3 up = glm::cross(right, forward);
glm::vec3 accel(0.0f);
if (movement_.forward_) accel += forward;
if (movement_.backward_) accel -= forward;
if (movement_.left_) accel -= right;
if (movement_.right_) accel += right;
if (movement_.up_) accel += up;
if (movement_.down_) accel -= up;
if (movement_.fastSpeed_) accel *= fastCoef_;
if (accel == glm::vec3(0))
{
// decelerate naturally according to the damping value
moveSpeed_ -= moveSpeed_ * std::min((1.0f / damping_) * static_cast<float>(deltaSeconds), 1.0f);
}
else
{
// acceleration
moveSpeed_ += accel * acceleration_ * static_cast<float>(deltaSeconds);
const float maxSpeed = movement_.fastSpeed_ ? maxSpeed_ * fastCoef_ : maxSpeed_;
if (glm::length(moveSpeed_) > maxSpeed) moveSpeed_ = glm::normalize(moveSpeed_) * maxSpeed;
}
cameraPosition_ += moveSpeed_ * static_cast<float>(deltaSeconds);
}
virtual glm::mat4 getViewMatrix() const override
{
const glm::mat4 t = glm::translate(glm::mat4(1.0f), -cameraPosition_);
const glm::mat4 r = glm::mat4_cast(cameraOrientation_);
return r * t;
}
virtual glm::vec3 getPosition() const override
{
return cameraPosition_;
}
void setPosition(const glm::vec3& pos)
{
cameraPosition_ = pos;
}
void setSpeed(const glm::vec3& speed) {
moveSpeed_ = speed;
}
void resetMousePosition(const glm::vec2& p) { mousePos_ = p; };
void setUpVector(const glm::vec3& up)
{
const glm::mat4 view = getViewMatrix();
const glm::vec3 dir = -glm::vec3(view[0][2], view[1][2], view[2][2]);
cameraOrientation_ = glm::lookAt(cameraPosition_, cameraPosition_ + dir, up);
}
inline void lookAt(const glm::vec3& pos, const glm::vec3& target, const glm::vec3& up) {
cameraPosition_ = pos;
cameraOrientation_ = glm::lookAt(pos, target, up);
}
public:
struct Movement
{
bool forward_ = false;
bool backward_ = false;
bool left_ = false;
bool right_ = false;
bool up_ = false;
bool down_ = false;
//
bool fastSpeed_ = false;
} movement_;
public:
float mouseSpeed_ = 4.0f;
float acceleration_ = 150.0f;
float damping_ = 0.2f;
float maxSpeed_ = 10.0f;
float fastCoef_ = 10.0f;
private:
glm::vec2 mousePos_ = glm::vec2(0);
glm::vec3 cameraPosition_ = glm::vec3(0.0f, 10.0f, 10.0f);
glm::quat cameraOrientation_ = glm::quat(glm::vec3(0));
glm::vec3 moveSpeed_ = glm::vec3(0.0f);
glm::vec3 up_ = glm::vec3(0.0f, 0.0f, 1.0f);
};
class CameraPositioner_MoveTo final : public CameraPositionerInterface
{
public:
CameraPositioner_MoveTo(const glm::vec3& pos, const glm::vec3& angles)
: positionCurrent_(pos)
, positionDesired_(pos)
, anglesCurrent_(angles)
, anglesDesired_(angles)
{}
void update(float deltaSeconds, const glm::vec2& mousePos, bool mousePressed)
{
positionCurrent_ += dampingLinear_ * deltaSeconds * (positionDesired_ - positionCurrent_);
// normalization is required to avoid "spinning" around the object 2pi times
anglesCurrent_ = clipAngles(anglesCurrent_);
anglesDesired_ = clipAngles(anglesDesired_);
// update angles
anglesCurrent_ -= angleDelta(anglesCurrent_, anglesDesired_) * dampingEulerAngles_ * deltaSeconds;
// normalize new angles
anglesCurrent_ = clipAngles(anglesCurrent_);
const glm::vec3 a = glm::radians(anglesCurrent_);
currentTransform_ = glm::translate(glm::yawPitchRoll(a.y, a.x, a.z), -positionCurrent_);
}
void setPosition(const glm::vec3& p) { positionCurrent_ = p; }
void setAngles(float pitch, float pan, float roll) { anglesCurrent_ = glm::vec3(pitch, pan, roll); }
void setAngles(const glm::vec3& angles) { anglesCurrent_ = angles; }
void setDesiredPosition(const glm::vec3& p) { positionDesired_ = p; }
void setDesiredAngles(float pitch, float pan, float roll) { anglesDesired_ = glm::vec3(pitch, pan, roll); }
void setDesiredAngles(const glm::vec3& angles) { anglesDesired_ = angles; }
virtual glm::vec3 getPosition() const override { return positionCurrent_; }
virtual glm::mat4 getViewMatrix() const override { return currentTransform_; }
public:
float dampingLinear_ = 10.0f;
glm::vec3 dampingEulerAngles_ = glm::vec3(5.0f, 5.0f, 5.0f);
private:
glm::vec3 positionCurrent_ = glm::vec3(0.0f);
glm::vec3 positionDesired_ = glm::vec3(0.0f);
/// pitch, pan, roll
glm::vec3 anglesCurrent_ = glm::vec3(0.0f);
glm::vec3 anglesDesired_ = glm::vec3(0.0f);
glm::mat4 currentTransform_ = glm::mat4(1.0f);
static inline float clipAngle(float d)
{
if (d < -180.0f) return d + 360.0f;
if (d > +180.0f) return d - 360.f;
return d;
}
static inline glm::vec3 clipAngles(const glm::vec3& angles)
{
return glm::vec3(
std::fmod(angles.x, 360.0f),
std::fmod(angles.y, 360.0f),
std::fmod(angles.z, 360.0f)
);
}
static inline glm::vec3 angleDelta(const glm::vec3& anglesCurrent, const glm::vec3& anglesDesired)
{
const glm::vec3 d = clipAngles(anglesCurrent) - clipAngles(anglesDesired);
return glm::vec3(clipAngle(d.x), clipAngle(d.y), clipAngle(d.z));
}
};

77
shared/Graph.h Normal file
View file

@ -0,0 +1,77 @@
#pragma once
#include <lvk/LVK.h>
#include <deque>
#include <vector>
class LinearGraph
{
public:
explicit LinearGraph(const char* name, size_t maxGraphPoints = 256)
: name_(name)
, maxPoints_(maxGraphPoints)
{
}
void addPoint(float value)
{
graph_.push_back(value);
if (graph_.size() > maxPoints_)
graph_.erase(graph_.begin());
}
void renderGraph(uint32_t x, uint32_t y, uint32_t width, uint32_t height, const glm::vec4& color = vec4(1.0)) const
{
LVK_PROFILER_FUNCTION();
float minVal = std::numeric_limits<float>::max();
float maxVal = std::numeric_limits<float>::min();
for (float f : graph_) {
if (f < minVal)
minVal = f;
if (f > maxVal)
maxVal = f;
}
const float range = maxVal - minVal;
float valX = 0.0;
std::vector<float> dataX_;
std::vector<float> dataY_;
dataX_.reserve(graph_.size());
dataY_.reserve(graph_.size());
for (float f : graph_) {
const float valY = (f - minVal) / range;
valX += 1.0f / maxPoints_;
dataX_.push_back(valX);
dataY_.push_back(valY);
}
ImGui::SetNextWindowPos(ImVec2(x, y));
ImGui::SetNextWindowSize(ImVec2(width, height));
ImGui::Begin(
name_, nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs);
if (ImPlot::BeginPlot(name_, ImVec2(width, height), ImPlotFlags_CanvasOnly | ImPlotFlags_NoFrame | ImPlotFlags_NoInputs)) {
ImPlot::SetupAxes(nullptr, nullptr, ImPlotAxisFlags_NoDecorations, ImPlotAxisFlags_NoDecorations);
ImPlot::PushStyleColor(ImPlotCol_Line, ImVec4(color.r, color.g, color.b, color.a));
ImPlot::PushStyleColor(ImPlotCol_PlotBg, ImVec4(0, 0, 0, 0));
ImPlot::PlotLine("#line", dataX_.data(), dataY_.data(), (int)graph_.size(), ImPlotLineFlags_None);
ImPlot::PopStyleColor(2);
ImPlot::EndPlot();
}
ImGui::End();
}
private:
const char* name_ = nullptr;
const size_t maxPoints_;
std::deque<float> graph_;
};

55
shared/HelpersGLFW.cpp Normal file
View file

@ -0,0 +1,55 @@
#include "HelpersGLFW.h"
GLFWwindow* initWindow(const char* windowTitle, uint32_t& outWidth, uint32_t& outHeight)
{
glfwSetErrorCallback([](int error, const char* description) { printf("GLFW Error (%i): %s\n", error, description); });
if (!glfwInit()) {
return nullptr;
}
const bool wantsWholeArea = !outWidth || !outHeight;
glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
glfwWindowHint(GLFW_RESIZABLE, wantsWholeArea ? GLFW_FALSE : GLFW_TRUE);
// render full screen without overlapping taskbar
GLFWmonitor* monitor = glfwGetPrimaryMonitor();
const GLFWvidmode* mode = glfwGetVideoMode(monitor);
int x = 0;
int y = 0;
int w = mode->width;
int h = mode->height;
if (wantsWholeArea) {
glfwGetMonitorWorkarea(monitor, &x, &y, &w, &h);
} else {
w = outWidth;
h = outHeight;
}
GLFWwindow* window = glfwCreateWindow(w, h, windowTitle, nullptr, nullptr);
if (!window) {
glfwTerminate();
return nullptr;
}
if (wantsWholeArea) {
glfwSetWindowPos(window, x, y);
}
glfwGetWindowSize(window, &w, &h);
glfwSetKeyCallback(window, [](GLFWwindow* window, int key, int, int action, int) {
if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) {
glfwSetWindowShouldClose(window, GLFW_TRUE);
}
});
outWidth = (uint32_t)w;
outHeight = (uint32_t)h;
return window;
}

24
shared/HelpersGLFW.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <stdio.h>
#define GLFW_INCLUDE_NONE
#include <GLFW/glfw3.h>
// clang-format off
#ifdef _WIN32
# define GLFW_EXPOSE_NATIVE_WIN32
# define GLFW_EXPOSE_NATIVE_WGL
#elif __APPLE__
# define GLFW_EXPOSE_NATIVE_COCOA
#elif defined(__linux__)
# define GLFW_EXPOSE_NATIVE_X11
#else
# error Unsupported OS
#endif
// clang-format on
#include <GLFW/glfw3native.h>
GLFWwindow *initWindow(const char *windowTitle, uint32_t &outWidth,
uint32_t &outHeight);

239
shared/LineCanvas.cpp Normal file
View file

@ -0,0 +1,239 @@
#include "LineCanvas.h"
static const char* codeVS = R"(
layout (location = 0) out vec4 out_color;
struct Vertex {
vec4 pos;
vec4 rgba;
};
layout(std430, buffer_reference) readonly buffer VertexBuffer {
Vertex vertices[];
};
layout(push_constant) uniform PushConstants {
mat4 mvp;
VertexBuffer vb;
};
void main() {
// Vertex v = vb.vertices[gl_VertexIndex]; <--- does not work on Snapdragon Adreno
out_color = vb.vertices[gl_VertexIndex].rgba;
gl_Position = mvp * vb.vertices[gl_VertexIndex].pos;
})";
static const char* codeFS = R"(
layout (location = 0) in vec4 in_color;
layout (location = 0) out vec4 out_color;
void main() {
out_color = in_color;
})";
void LineCanvas2D::render(const char* nameImGuiWindow)
{
LVK_PROFILER_FUNCTION();
ImGui::SetNextWindowPos(ImVec2(0, 0));
ImGui::SetNextWindowSize(ImGui::GetMainViewport()->Size);
ImGui::Begin(
nameImGuiWindow, nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoInputs);
ImDrawList* drawList = ImGui::GetBackgroundDrawList();
for (const LineData& l : lines_) {
drawList->AddLine(ImVec2(l.p1.x, l.p1.y), ImVec2(l.p2.x, l.p2.y), ImColor(l.color.r, l.color.g, l.color.b, l.color.a));
}
ImGui::End();
}
void LineCanvas3D::line(const vec3& p1, const vec3& p2, const vec4& c)
{
lines_.push_back({ .pos = vec4(p1, 1.0f), .color = c });
lines_.push_back({ .pos = vec4(p2, 1.0f), .color = c });
}
void LineCanvas3D::plane(
const vec3& o, const vec3& v1, const vec3& v2, int n1, int n2, float s1, float s2, const vec4& color, const vec4& outlineColor)
{
line(o - s1 / 2.0f * v1 - s2 / 2.0f * v2, o - s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor);
line(o + s1 / 2.0f * v1 - s2 / 2.0f * v2, o + s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor);
line(o - s1 / 2.0f * v1 + s2 / 2.0f * v2, o + s1 / 2.0f * v1 + s2 / 2.0f * v2, outlineColor);
line(o - s1 / 2.0f * v1 - s2 / 2.0f * v2, o + s1 / 2.0f * v1 - s2 / 2.0f * v2, outlineColor);
for (int i = 1; i < n1; i++) {
float t = ((float)i - (float)n1 / 2.0f) * s1 / (float)n1;
const vec3 o1 = o + t * v1;
line(o1 - s2 / 2.0f * v2, o1 + s2 / 2.0f * v2, color);
}
for (int i = 1; i < n2; i++) {
const float t = ((float)i - (float)n2 / 2.0f) * s2 / (float)n2;
const vec3 o2 = o + t * v2;
line(o2 - s1 / 2.0f * v1, o2 + s1 / 2.0f * v1, color);
}
}
void LineCanvas3D::box(const mat4& m, const vec3& size, const vec4& c)
{
vec3 pts[8] = {
vec3(+size.x, +size.y, +size.z), vec3(+size.x, +size.y, -size.z), vec3(+size.x, -size.y, +size.z), vec3(+size.x, -size.y, -size.z),
vec3(-size.x, +size.y, +size.z), vec3(-size.x, +size.y, -size.z), vec3(-size.x, -size.y, +size.z), vec3(-size.x, -size.y, -size.z),
};
for (auto& p : pts)
p = vec3(m * vec4(p, 1.f));
line(pts[0], pts[1], c);
line(pts[2], pts[3], c);
line(pts[4], pts[5], c);
line(pts[6], pts[7], c);
line(pts[0], pts[2], c);
line(pts[1], pts[3], c);
line(pts[4], pts[6], c);
line(pts[5], pts[7], c);
line(pts[0], pts[4], c);
line(pts[1], pts[5], c);
line(pts[2], pts[6], c);
line(pts[3], pts[7], c);
}
void LineCanvas3D::box(const mat4& m, const BoundingBox& box, const glm::vec4& color)
{
this->box(m * glm::translate(mat4(1.f), .5f * (box.min_ + box.max_)), 0.5f * vec3(box.max_ - box.min_), color);
}
void LineCanvas3D::frustum(const mat4& camView, const mat4& camProj, const vec4& color)
{
const vec3 corners[] = { vec3(-1, -1, -1), vec3(+1, -1, -1), vec3(+1, +1, -1), vec3(-1, +1, -1),
vec3(-1, -1, +1), vec3(+1, -1, +1), vec3(+1, +1, +1), vec3(-1, +1, +1) };
vec3 pp[8];
for (int i = 0; i < 8; i++) {
vec4 q = glm::inverse(camView) * glm::inverse(camProj) * vec4(corners[i], 1.0f);
pp[i] = vec3(q.x / q.w, q.y / q.w, q.z / q.w);
}
line(pp[0], pp[4], color);
line(pp[1], pp[5], color);
line(pp[2], pp[6], color);
line(pp[3], pp[7], color);
// near
line(pp[0], pp[1], color);
line(pp[1], pp[2], color);
line(pp[2], pp[3], color);
line(pp[3], pp[0], color);
// x
line(pp[0], pp[2], color);
line(pp[1], pp[3], color);
// far
line(pp[4], pp[5], color);
line(pp[5], pp[6], color);
line(pp[6], pp[7], color);
line(pp[7], pp[4], color);
// x
line(pp[4], pp[6], color);
line(pp[5], pp[7], color);
const vec4 gridColor = color * 0.7f;
const int gridLines = 100;
// bottom
{
vec3 p1 = pp[0];
vec3 p2 = pp[1];
const vec3 s1 = (pp[4] - pp[0]) / float(gridLines);
const vec3 s2 = (pp[5] - pp[1]) / float(gridLines);
for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
line(p1, p2, gridColor);
}
// top
{
vec3 p1 = pp[2];
vec3 p2 = pp[3];
const vec3 s1 = (pp[6] - pp[2]) / float(gridLines);
const vec3 s2 = (pp[7] - pp[3]) / float(gridLines);
for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
line(p1, p2, gridColor);
}
// left
{
vec3 p1 = pp[0];
vec3 p2 = pp[3];
const vec3 s1 = (pp[4] - pp[0]) / float(gridLines);
const vec3 s2 = (pp[7] - pp[3]) / float(gridLines);
for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
line(p1, p2, gridColor);
}
// right
{
vec3 p1 = pp[1];
vec3 p2 = pp[2];
const vec3 s1 = (pp[5] - pp[1]) / float(gridLines);
const vec3 s2 = (pp[6] - pp[2]) / float(gridLines);
for (int i = 0; i != gridLines; i++, p1 += s1, p2 += s2)
line(p1, p2, gridColor);
}
}
void LineCanvas3D::render(lvk::IContext& ctx, const lvk::Framebuffer& desc, lvk::ICommandBuffer& buf, uint32_t numSamples)
{
LVK_PROFILER_FUNCTION();
if (lines_.empty()) {
return;
}
const uint32_t requiredSize = lines_.size() * sizeof(LineData);
if (currentBufferSize_[currentFrame_] < requiredSize) {
linesBuffer_[currentFrame_] = ctx.createBuffer(
{ .usage = lvk::BufferUsageBits_Storage, .storage = lvk::StorageType_HostVisible, .size = requiredSize, .data = lines_.data() });
currentBufferSize_[currentFrame_] = requiredSize;
} else {
ctx.upload(linesBuffer_[currentFrame_], lines_.data(), requiredSize);
}
if (pipeline_.empty() || pipelineSamples_ != numSamples) {
pipelineSamples_ = numSamples;
vert_ = ctx.createShaderModule({ codeVS, lvk::Stage_Vert, "Shader Module: imgui (vert)" });
frag_ = ctx.createShaderModule({ codeFS, lvk::Stage_Frag, "Shader Module: imgui (frag)" });
pipeline_ = ctx.createRenderPipeline(
{
.topology = lvk::Topology_Line,
.smVert = vert_,
.smFrag = frag_,
.color = { {
.format = ctx.getFormat(desc.color[0].texture),
.blendEnabled = true,
.srcRGBBlendFactor = lvk::BlendFactor_SrcAlpha,
.dstRGBBlendFactor = lvk::BlendFactor_OneMinusSrcAlpha,
} },
.depthFormat = desc.depthStencil.texture ? ctx.getFormat(desc.depthStencil.texture) : lvk::Format_Invalid,
.cullMode = lvk::CullMode_None,
.samplesCount = numSamples,
},
nullptr);
}
struct {
mat4 mvp;
uint64_t addr;
} pc{
.mvp = mvp_,
.addr = ctx.gpuAddress(linesBuffer_[currentFrame_]),
};
buf.cmdBindRenderPipeline(pipeline_);
buf.cmdPushConstants(pc);
buf.cmdDraw(lines_.size());
currentFrame_ = (currentFrame_ + 1) % LVK_ARRAY_NUM_ELEMENTS(linesBuffer_);
}

54
shared/LineCanvas.h Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include "shared/VulkanApp.h"
class LineCanvas2D
{
public:
void clear() { lines_.clear(); }
void line(const vec2& p1, const vec2& p2, const vec4& c) { lines_.push_back({ .p1 = p1, .p2 = p2, .color = c }); }
void render(const char* nameImGuiWindow);
private:
struct LineData {
vec2 p1, p2;
vec4 color;
};
std::vector<LineData> lines_;
};
class LineCanvas3D
{
public:
void clear() { lines_.clear(); }
void line(const vec3& p1, const vec3& p2, const vec4& c);
void plane(
const vec3& orig, const vec3& v1, const vec3& v2, int n1, int n2, float s1, float s2, const vec4& color, const vec4& outlineColor);
void box(const mat4& m, const BoundingBox& box, const vec4& color);
void box(const mat4& m, const vec3& size, const vec4& color);
void frustum(const mat4& camView, const mat4& camProj, const vec4& color);
void setMatrix(const mat4& mvp) { mvp_ = mvp; }
void render(lvk::IContext& ctx, const lvk::Framebuffer& desc, lvk::ICommandBuffer& buf, uint32_t numSamples = 1);
private:
mat4 mvp_ = mat4(1.0f);
struct LineData {
vec4 pos;
vec4 color;
};
std::vector<LineData> lines_;
lvk::Holder<lvk::ShaderModuleHandle> vert_;
lvk::Holder<lvk::ShaderModuleHandle> frag_;
lvk::Holder<lvk::RenderPipelineHandle> pipeline_;
lvk::Holder<lvk::BufferHandle> linesBuffer_[3] = {};
uint32_t pipelineSamples_ = 1;
uint32_t currentBufferSize_[3] = {};
uint32_t currentFrame_ = 0;
static_assert(LVK_ARRAY_NUM_ELEMENTS(linesBuffer_) == LVK_ARRAY_NUM_ELEMENTS(currentBufferSize_));
};

151
shared/Scene/MergeUtil.cpp Normal file
View file

@ -0,0 +1,151 @@
#include "shared/Scene/MergeUtil.h"
#include "shared/Scene/Scene.h"
#include <unordered_map>
static uint32_t shiftMeshIndices(MeshData& meshData, const std::vector<uint32_t>& meshesToMerge)
{
uint32_t minVtxOffset = std::numeric_limits<uint32_t>::max();
for (uint32_t i : meshesToMerge)
minVtxOffset = std::min(meshData.meshes[i].vertexOffset, minVtxOffset);
uint32_t mergeCount = 0u; // calculated by summing index counts in meshesToMerge
// now shift all the indices in individual index blocks [use minVtxOffset]
for (uint32_t i : meshesToMerge) {
Mesh& m = meshData.meshes[i];
// for how much should we shift the indices in mesh [m]
const uint32_t delta = m.vertexOffset - minVtxOffset;
const uint32_t idxCount = m.getLODIndicesCount(0);
for (uint32_t ii = 0u; ii < idxCount; ii++)
meshData.indexData[m.indexOffset + ii] += delta;
m.vertexOffset = minVtxOffset;
// sum all the deleted meshes' indices
mergeCount += idxCount;
}
return meshData.indexData.size() - mergeCount;
}
// All the meshesToMerge now have the same vertexOffset and individual index values are shifted by appropriate amount
// Here we move all the indices to appropriate places in the new index array
static void mergeIndexArray(MeshData& md, const std::vector<uint32_t>& meshesToMerge, std::unordered_map<uint32_t, uint32_t>& oldToNew)
{
std::vector<uint32_t> newIndices(md.indexData.size());
// Two offsets in the new indices array (one begins at the start, the second one after all the copied indices)
uint32_t copyOffset = 0;
uint32_t mergeOffset = shiftMeshIndices(md, meshesToMerge);
const size_t mergedMeshIndex = md.meshes.size() - meshesToMerge.size();
uint32_t newIndex = 0u;
for (size_t midx = 0u; midx < md.meshes.size(); midx++) {
const bool shouldMerge = std::binary_search(meshesToMerge.begin(), meshesToMerge.end(), midx);
oldToNew[midx] = shouldMerge ? mergedMeshIndex : newIndex;
newIndex += shouldMerge ? 0 : 1;
Mesh& mesh = md.meshes[midx];
const uint32_t idxCount = mesh.getLODIndicesCount(0);
// move all indices to the new array at mergeOffset
const auto start = md.indexData.begin() + mesh.indexOffset;
mesh.indexOffset = copyOffset;
uint32_t* const offsetPtr = shouldMerge ? &mergeOffset : &copyOffset;
std::copy(start, start + idxCount, newIndices.begin() + *offsetPtr);
*offsetPtr += idxCount;
}
md.indexData = newIndices;
// all the merged indices are now in lastMesh
Mesh lastMesh = md.meshes[meshesToMerge[0]];
lastMesh.indexOffset = copyOffset;
lastMesh.lodOffset[0] = copyOffset;
lastMesh.lodOffset[1] = mergeOffset;
lastMesh.lodCount = 1;
md.meshes.push_back(lastMesh);
}
void mergeNodesWithMaterial(Scene& scene, MeshData& meshData, const std::string& materialName)
{
// Find material index
const int oldMaterial = (int)std::distance(
std::begin(scene.materialNames), std::find(std::begin(scene.materialNames), std::end(scene.materialNames), materialName));
std::vector<uint32_t> toDelete;
for (size_t i = 0u; i < scene.hierarchy.size(); i++)
if (scene.meshForNode.contains(i) && scene.materialForNode.contains(i) && (scene.materialForNode.at(i) == oldMaterial))
toDelete.push_back(i);
std::vector<uint32_t> meshesToMerge(toDelete.size());
// Convert toDelete indices to mesh indices
std::transform(toDelete.begin(), toDelete.end(), meshesToMerge.begin(), [&scene](uint32_t i) { return scene.meshForNode.at(i); });
// TODO: if merged mesh transforms are non-zero, then we should pre-transform individual mesh vertices in meshData using local transform
// old-to-new mesh indices
std::unordered_map<uint32_t, uint32_t> oldToNew;
// now move all the meshesToMerge to the end of array
mergeIndexArray(meshData, meshesToMerge, oldToNew);
// cutoff all but one of the merged meshes (insert the last saved mesh from meshesToMerge - they are all the same)
eraseSelected(meshData.meshes, meshesToMerge);
for (auto& n : scene.meshForNode)
n.second = oldToNew[n.second];
// reattach the node with merged meshes [identity transforms are assumed]
int newNode = addNode(scene, 0, 1);
scene.meshForNode[newNode] = (int)meshData.meshes.size() - 1;
scene.materialForNode[newNode] = (uint32_t)oldMaterial;
deleteSceneNodes(scene, toDelete);
}
void mergeMaterialLists(
const std::vector<std::vector<Material>*>& oldMaterials, const std::vector<std::vector<std::string>*>& oldTextures,
std::vector<Material>& allMaterials, std::vector<std::string>& newTextures)
{
// map texture names to indices in newTexturesList (calculated as we fill the newTexturesList)
std::unordered_map<std::string, int> newTextureNames;
std::unordered_map<size_t, size_t> materialToTextureList; // use the index of Material in the allMaterials array
// create a combined material list [no hashing of materials, just straightforward merging of all lists]
for (size_t midx = 0; midx != oldMaterials.size(); midx++) {
for (const Material& m : *oldMaterials[midx]) {
allMaterials.push_back(m);
materialToTextureList[allMaterials.size() - 1] = midx;
}
}
// create one combined texture list
for (const std::vector<std::string>* tl : oldTextures) {
for (const std::string& file : *tl) {
newTextureNames[file] = addUnique(newTextures, file);
}
}
// a lambda to replace textureID by a new "version" (from the global list)
auto replaceTexture = [&materialToTextureList, &oldTextures, &newTextureNames](int mtlId, int* textureID) {
if (*textureID == -1)
return;
const size_t listIdx = materialToTextureList[mtlId];
const std::vector<std::string>& texList = *oldTextures[listIdx];
const std::string& texFile = texList[*textureID];
*textureID = newTextureNames[texFile];
};
for (size_t i = 0; i < allMaterials.size(); i++) {
Material& m = allMaterials[i];
replaceTexture(i, &m.baseColorTexture);
replaceTexture(i, &m.emissiveTexture);
replaceTexture(i, &m.normalTexture);
replaceTexture(i, &m.opacityTexture);
}
}

16
shared/Scene/MergeUtil.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include "shared/Scene/Scene.h"
#include "shared/Scene/VtxData.h"
void mergeNodesWithMaterial(Scene& scene, MeshData& meshData, const std::string& materialName);
// Merge material lists from multiple scenes (follows the logic of merging in mergeScenes)
void mergeMaterialLists(
// Input:
const std::vector<std::vector<Material>*>& oldMaterials, // all materials
const std::vector<std::vector<std::string>*>& oldTextures, // all textures from all material lists
// Output:
std::vector<Material>& allMaterials,
std::vector<std::string>& newTextures // all textures (merged from oldTextures, only unique items)
);

493
shared/Scene/Scene.cpp Normal file
View file

@ -0,0 +1,493 @@
#include "shared/Scene/Scene.h"
#include "shared/Utils.h"
#include <algorithm>
#include <numeric>
int addNode(Scene &scene, int parent, int level) {
const int node = (int)scene.hierarchy.size();
{
// TODO: resize aux arrays (local/global etc.)
scene.localTransform.push_back(glm::mat4(1.0f));
scene.globalTransform.push_back(glm::mat4(1.0f));
}
scene.hierarchy.push_back({.parent = parent, .lastSibling = -1});
if (parent > -1) {
// find first item (sibling)
const int s = scene.hierarchy[parent].firstChild;
if (s == -1) {
scene.hierarchy[parent].firstChild = node;
scene.hierarchy[node].lastSibling = node;
} else {
int dest = scene.hierarchy[s].lastSibling;
if (dest <= -1) {
// no cached lastSibling, iterate nextSibling indices
for (dest = s; scene.hierarchy[dest].nextSibling != -1;
dest = scene.hierarchy[dest].nextSibling)
;
}
scene.hierarchy[dest].nextSibling = node;
scene.hierarchy[s].lastSibling = node;
}
}
scene.hierarchy[node].level = level;
scene.hierarchy[node].nextSibling = -1;
scene.hierarchy[node].firstChild = -1;
return node;
}
void markAsChanged(Scene &scene, int node) {
const int level = scene.hierarchy[node].level;
scene.changedAtThisFrame[level].push_back(node);
// TODO: use non-recursive iteration with aux stack
for (int s = scene.hierarchy[node].firstChild; s != -1;
s = scene.hierarchy[s].nextSibling) {
markAsChanged(scene, s);
}
}
int findNodeByName(const Scene &scene, const std::string &name) {
// Extremely simple linear search without any hierarchy reference
// To support DFS/BFS searches separate traversal routines are needed
for (size_t i = 0; i < scene.localTransform.size(); i++)
if (scene.nameForNode.contains(i)) {
int strID = scene.nameForNode.at(i);
if (strID > -1)
if (scene.nodeNames[strID] == name)
return (int)i;
}
return -1;
}
bool mat4IsIdentity(const glm::mat4 &m);
void fprintfMat4(FILE *f, const glm::mat4 &m);
// CPU version of global transform update []
bool recalculateGlobalTransforms(Scene &scene) {
bool wasUpdated = false;
if (!scene.changedAtThisFrame[0].empty()) {
const int c = scene.changedAtThisFrame[0][0];
scene.globalTransform[c] = scene.localTransform[c];
scene.changedAtThisFrame[0].clear();
wasUpdated = true;
}
for (int i = 1; i < MAX_NODE_LEVEL; i++) {
for (int c : scene.changedAtThisFrame[i]) {
const int p = scene.hierarchy[c].parent;
scene.globalTransform[c] =
scene.globalTransform[p] * scene.localTransform[c];
}
wasUpdated |= !scene.changedAtThisFrame[i].empty();
scene.changedAtThisFrame[i].clear();
}
return wasUpdated;
}
void loadMap(FILE *f, std::unordered_map<uint32_t, uint32_t> &map) {
std::vector<uint32_t> ms;
uint32_t sz = 0;
fread(&sz, 1, sizeof(sz), f);
ms.resize(sz);
fread(ms.data(), sizeof(uint32_t), sz, f);
for (size_t i = 0; i < (sz / 2); i++)
map[ms[i * 2 + 0]] = ms[i * 2 + 1];
}
void loadScene(const char *fileName, Scene &scene) {
FILE *f = fopen(fileName, "rb");
if (!f) {
printf("Cannot open scene file '%s'. Please run SceneConverter from "
"Chapter7 and/or MergeMeshes from Chapter 9",
fileName);
return;
}
uint32_t sz = 0;
fread(&sz, sizeof(sz), 1, f);
scene.hierarchy.resize(sz);
scene.globalTransform.resize(sz);
scene.localTransform.resize(sz);
// TODO: check > -1
// TODO: recalculate changedAtThisLevel() - find max depth of a node [or save
// scene.maxLevel]
fread(scene.localTransform.data(), sizeof(glm::mat4), sz, f);
fread(scene.globalTransform.data(), sizeof(glm::mat4), sz, f);
fread(scene.hierarchy.data(), sizeof(Hierarchy), sz, f);
// Mesh for node [index to some list of buffers]
loadMap(f, scene.materialForNode);
loadMap(f, scene.meshForNode);
if (!feof(f)) {
loadMap(f, scene.nameForNode);
loadStringList(f, scene.nodeNames);
loadStringList(f, scene.materialNames);
}
fclose(f);
markAsChanged(scene, 0);
recalculateGlobalTransforms(scene);
}
void saveMap(FILE *f, const std::unordered_map<uint32_t, uint32_t> &map) {
std::vector<uint32_t> ms;
ms.reserve(map.size() * 2);
for (const auto &m : map) {
ms.push_back(m.first);
ms.push_back(m.second);
}
const uint32_t sz = static_cast<uint32_t>(ms.size());
fwrite(&sz, sizeof(sz), 1, f);
fwrite(ms.data(), sizeof(uint32_t), ms.size(), f);
}
void saveScene(const char *fileName, const Scene &scene) {
FILE *f = fopen(fileName, "wb");
const uint32_t sz = (uint32_t)scene.hierarchy.size();
fwrite(&sz, sizeof(sz), 1, f);
fwrite(scene.localTransform.data(), sizeof(glm::mat4), sz, f);
fwrite(scene.globalTransform.data(), sizeof(glm::mat4), sz, f);
fwrite(scene.hierarchy.data(), sizeof(Hierarchy), sz, f);
// Mesh for node [index to some list of buffers]
saveMap(f, scene.materialForNode);
saveMap(f, scene.meshForNode);
if (!scene.nodeNames.empty() && !scene.nameForNode.empty()) {
saveMap(f, scene.nameForNode);
saveStringList(f, scene.nodeNames);
saveStringList(f, scene.materialNames);
}
fclose(f);
}
bool mat4IsIdentity(const glm::mat4 &m) {
return (m[0][0] == 1 && m[0][1] == 0 && m[0][2] == 0 && m[0][3] == 0 &&
m[1][0] == 0 && m[1][1] == 1 && m[1][2] == 0 && m[1][3] == 0 &&
m[2][0] == 0 && m[2][1] == 0 && m[2][2] == 1 && m[2][3] == 0 &&
m[3][0] == 0 && m[3][1] == 0 && m[3][2] == 0 && m[3][3] == 1);
}
void fprintfMat4(FILE *f, const glm::mat4 &m) {
if (mat4IsIdentity(m)) {
fprintf(f, "Identity\n");
} else {
fprintf(f, "\n");
for (int i = 0; i < 4; i++) {
for (int j = 0; j < 4; j++)
fprintf(f, "%f ;", m[i][j]);
fprintf(f, "\n");
}
}
}
void dumpTransforms(const char *fileName, const Scene &scene) {
FILE *f = fopen(fileName, "a+");
for (size_t i = 0; i < scene.localTransform.size(); i++) {
fprintf(f, "Node[%d].localTransform: ", (int)i);
fprintfMat4(f, scene.localTransform[i]);
fprintf(f, "Node[%d].globalTransform: ", (int)i);
fprintfMat4(f, scene.globalTransform[i]);
fprintf(f, "Node[%d].globalDet = %f; localDet = %f\n", (int)i,
glm::determinant(scene.globalTransform[i]),
glm::determinant(scene.localTransform[i]));
}
fclose(f);
}
void printChangedNodes(const Scene &scene) {
for (int i = 0; i < MAX_NODE_LEVEL && (!scene.changedAtThisFrame[i].empty());
i++) {
printf("Changed at level(%d):\n", i);
for (const int &c : scene.changedAtThisFrame[i]) {
int p = scene.hierarchy[c].parent;
// scene.globalTransform_[c] = scene.globalTransform_[p] *
// scene.localTransform_[c];
printf(" Node %d. Parent = %d; LocalTransform: ", c, p);
fprintfMat4(stdout, scene.localTransform[i]);
if (p > -1) {
printf(" ParentGlobalTransform: ");
fprintfMat4(stdout, scene.globalTransform[p]);
}
}
}
}
// Shift all hierarchy components in the nodes
void shiftNodes(Scene &scene, int startOffset, int nodeCount, int shiftAmount) {
auto shiftNode = [shiftAmount](Hierarchy &node) {
if (node.parent > -1)
node.parent += shiftAmount;
if (node.firstChild > -1)
node.firstChild += shiftAmount;
if (node.nextSibling > -1)
node.nextSibling += shiftAmount;
if (node.lastSibling > -1)
node.lastSibling += shiftAmount;
// node->level does not require to be shifted
};
// If there are too many nodes, we can use std::execution::par with
// std::transform:
// std::transform(scene.hierarchy_.begin() + startOffset,
// scene.hierarchy_.begin() + nodeCount,
// scene.hierarchy_.begin() + startOffset,
// shiftNode);
// for (auto i = scene.hierarchy_.begin() + startOffset ; i !=
// scene.hierarchy_.begin() + nodeCount ; i++) shiftNode(*i);
for (int i = 0; i < nodeCount; i++)
shiftNode(scene.hierarchy[i + startOffset]);
}
using ItemMap = std::unordered_map<uint32_t, uint32_t>;
// Add the items from otherMap shifting indices and values along the way
void mergeMaps(ItemMap &m, const ItemMap &otherMap, int indexOffset,
int itemOffset) {
for (const auto &i : otherMap)
m[i.first + indexOffset] = i.second + itemOffset;
}
/**
There are different use cases for scene merging.
The simplest one is the direct "gluing" of multiple scenes into one [all the
material lists and mesh lists are merged and indices in all scene nodes are
shifted appropriately] The second one is creating a "grid" of objects (or
scenes) with the same material and mesh sets. For the second use case we need
two flags: 'mergeMeshes' and 'mergeMaterials' to avoid shifting mesh indices
*/
void mergeScenes(Scene &scene, const std::vector<Scene *> &scenes,
const std::vector<glm::mat4> &rootTransforms,
const std::vector<uint32_t> &meshCounts, bool mergeMeshes,
bool mergeMaterials) {
// Create new root node
scene.hierarchy = {{
.parent = -1,
.firstChild = 1,
.nextSibling = -1,
.lastSibling = -1,
.level = 0,
}};
scene.nameForNode[0] = 0;
scene.nodeNames = {"NewRoot"};
scene.localTransform.push_back(glm::mat4(1.f));
scene.globalTransform.push_back(glm::mat4(1.f));
if (scenes.empty())
return;
int offs = 1;
int meshOffs = 0;
int nameOffs = (int)scene.nodeNames.size();
int materialOfs = 0;
auto meshCount = meshCounts.begin();
if (!mergeMaterials)
scene.materialNames = scenes[0]->materialNames;
// FIXME: too much logic (for all the components in a scene, though mesh data
// and materials go separately - they're dedicated data lists)
for (const Scene *s : scenes) {
mergeVectors(scene.localTransform, s->localTransform);
mergeVectors(scene.globalTransform, s->globalTransform);
mergeVectors(scene.hierarchy, s->hierarchy);
mergeVectors(scene.nodeNames, s->nodeNames);
if (mergeMaterials)
mergeVectors(scene.materialNames, s->materialNames);
const int nodeCount = (int)s->hierarchy.size();
shiftNodes(scene, offs, nodeCount, offs);
mergeMaps(scene.meshForNode, s->meshForNode, offs,
mergeMeshes ? meshOffs : 0);
mergeMaps(scene.materialForNode, s->materialForNode, offs,
mergeMaterials ? materialOfs : 0);
mergeMaps(scene.nameForNode, s->nameForNode, offs, nameOffs);
offs += nodeCount;
materialOfs += (int)s->materialNames.size();
nameOffs += (int)s->nodeNames.size();
if (mergeMeshes) {
meshOffs += *meshCount;
meshCount++;
}
}
// fixing 'nextSibling' fields in the old roots (zero-index in all the scenes)
offs = 1;
int idx = 0;
for (const Scene *s : scenes) {
const int nodeCount = (int)s->hierarchy.size();
const bool isLast = (idx == scenes.size() - 1);
// calculate new next sibling for the old scene roots
const int next = isLast ? -1 : offs + nodeCount;
scene.hierarchy[offs].nextSibling = next;
// attach to new root
scene.hierarchy[offs].parent = 0;
// transform old root nodes, if the transforms are given
if (!rootTransforms.empty())
scene.localTransform[offs] =
rootTransforms[idx] * scene.localTransform[offs];
offs += nodeCount;
idx++;
}
// now, shift levels of all nodes below the root
for (auto i = scene.hierarchy.begin() + 1; i != scene.hierarchy.end(); i++)
i->level++;
}
void dumpSceneToDot(const char *fileName, const Scene &scene, int *visited) {
FILE *f = fopen(fileName, "w");
fprintf(f, "digraph G\n{\n");
for (size_t i = 0; i < scene.globalTransform.size(); i++) {
std::string name = "";
std::string extra = "";
if (scene.nameForNode.contains(i)) {
int strID = scene.nameForNode.at(i);
name = scene.nodeNames[strID];
}
if (visited) {
if (visited[i])
extra = ", color = red";
}
fprintf(f, "n%d [label=\"%s\" %s]\n", (int)i, name.c_str(), extra.c_str());
}
for (size_t i = 0; i < scene.hierarchy.size(); i++) {
int p = scene.hierarchy[i].parent;
if (p > -1)
fprintf(f, "\t n%d -> n%d\n", p, (int)i);
}
fprintf(f, "}\n");
fclose(f);
}
// A rather long algorithm (and the auxiliary routines) to delete a number of
// scene nodes from the hierarchy
// Add an index to a sorted index array
static void addUniqueIdx(std::vector<uint32_t> &v, uint32_t index) {
if (!std::binary_search(v.begin(), v.end(), index))
v.push_back(index);
}
// Recurse down from a node and collect all nodes which are already marked for
// deletion
static void collectNodesToDelete(const Scene &scene, int node,
std::vector<uint32_t> &nodes) {
for (int n = scene.hierarchy[node].firstChild; n != -1;
n = scene.hierarchy[n].nextSibling) {
addUniqueIdx(nodes, n);
collectNodesToDelete(scene, n, nodes);
}
}
int findLastNonDeletedItem(const Scene &scene,
const std::vector<int> &newIndices, int node) {
// we have to be more subtle:
// if the (newIndices[firstChild_] == -1), we should follow the link and
// extract the last non-removed item
// ..
if (node == -1)
return -1;
return (newIndices[node] == -1)
? findLastNonDeletedItem(scene, newIndices,
scene.hierarchy[node].nextSibling)
: newIndices[node];
}
void shiftMapIndices(std::unordered_map<uint32_t, uint32_t> &items,
const std::vector<int> &newIndices) {
std::unordered_map<uint32_t, uint32_t> newItems;
for (const auto &m : items) {
int newIndex = newIndices[m.first];
if (newIndex != -1)
newItems[newIndex] = m.second;
}
items = newItems;
}
// Approximately an O ( N * Log(N) * Log(M)) algorithm (N = scene.size, M =
// nodesToDelete.size) to delete a collection of nodes from scene graph
void deleteSceneNodes(Scene &scene,
const std::vector<uint32_t> &nodesToDelete) {
// 0) Add all the nodes down below in the hierarchy
auto indicesToDelete = nodesToDelete;
for (uint32_t i : indicesToDelete)
collectNodesToDelete(scene, i, indicesToDelete);
// aux array with node indices to keep track of the moved ones [moved =
// [](node) { return (node != nodes[node]); ]
std::vector<int> nodes(scene.hierarchy.size());
std::iota(nodes.begin(), nodes.end(), 0);
// 1.a) Move all the indicesToDelete to the end of 'nodes' array (and cut them
// off, a variation of swap'n'pop for multiple elements)
const size_t oldSize = nodes.size();
eraseSelected(nodes, indicesToDelete);
// 1.b) Make a newIndices[oldIndex] mapping table
std::vector<int> newIndices(oldSize, -1);
for (int i = 0; i < nodes.size(); i++)
newIndices[nodes[i]] = i;
// 2) Replace all non-null parent/firstChild/nextSibling pointers in all the
// nodes by new positions
auto nodeMover = [&scene, &newIndices](Hierarchy &h) {
return Hierarchy{
.parent = (h.parent != -1) ? newIndices[h.parent] : -1,
.firstChild = findLastNonDeletedItem(scene, newIndices, h.firstChild),
.nextSibling = findLastNonDeletedItem(scene, newIndices, h.nextSibling),
.lastSibling = findLastNonDeletedItem(scene, newIndices, h.lastSibling),
};
};
std::transform(scene.hierarchy.begin(), scene.hierarchy.end(),
scene.hierarchy.begin(), nodeMover);
// 3) Finally throw away the hierarchy items
eraseSelected(scene.hierarchy, indicesToDelete);
// 4) As in mergeScenes() routine we also have to adjust all the "components"
// (i.e., meshes, materials, names and transformations)
// 4a) Transformations are stored in arrays, so we just erase the items as we
// did with the scene.hierarchy_
eraseSelected(scene.localTransform, indicesToDelete);
eraseSelected(scene.globalTransform, indicesToDelete);
// 4b) All the maps should change the key values with the newIndices[] array
shiftMapIndices(scene.meshForNode, newIndices);
shiftMapIndices(scene.materialForNode, newIndices);
shiftMapIndices(scene.nameForNode, newIndices);
// 5) scene node names list is not modified, but in principle it can be
// (remove all non-used items and adjust the nameForNode_ map) 6) Material
// names list is not modified also, but if some materials fell out of use
}

98
shared/Scene/Scene.h Normal file
View file

@ -0,0 +1,98 @@
#pragma once
#include <string>
#include <unordered_map>
#include <vector>
#include <glm/ext.hpp>
#include <glm/glm.hpp>
using glm::mat4;
// we do not define std::vector<Node*> Children - this is already present in the
// aiNode from assimp
constexpr const int MAX_NODE_LEVEL = 16;
struct Hierarchy {
// parent for this node (or -1 for root)
int parent = -1;
// first child for a node (or -1)
int firstChild = -1;
// next sibling for a node (or -1)
int nextSibling = -1;
// last added node (or -1)
int lastSibling = -1;
// cached node level
int level = 0;
};
/* This scene is converted into a descriptorSet(s) in MultiRenderer class
This structure is also used as a storage type in SceneExporter tool
*/
struct Scene {
// local transformations for each node and global transforms
// + an array of 'dirty/changed' local transforms
std::vector<mat4> localTransform; // indexed by node
std::vector<mat4> globalTransform; // indexed by node
// list of nodes that need their global transforms recalculated
std::vector<int> changedAtThisFrame[MAX_NODE_LEVEL];
// Hierarchy component
std::vector<Hierarchy> hierarchy;
// Mesh component: which Mesh belongs to which node (Node -> Mesh)
std::unordered_map<uint32_t, uint32_t> meshForNode;
// Material component: which material belongs to which node (Node -> Material)
std::unordered_map<uint32_t, uint32_t> materialForNode;
// Node name component: which name is assigned to the node (Node -> Name)
std::unordered_map<uint32_t, uint32_t> nameForNode;
// List of scene node names
std::vector<std::string> nodeNames;
// Debug list of material names
std::vector<std::string> materialNames;
};
int addNode(Scene &scene, int parent, int level);
void markAsChanged(Scene &scene, int node);
int findNodeByName(const Scene &scene, const std::string &name);
inline std::string getNodeName(const Scene &scene, int node) {
int strID =
scene.nameForNode.contains(node) ? scene.nameForNode.at(node) : -1;
return (strID > -1) ? scene.nodeNames[strID] : std::string();
}
inline void setNodeName(Scene &scene, int node, const std::string &name) {
uint32_t stringID = (uint32_t)scene.nodeNames.size();
scene.nodeNames.push_back(name);
scene.nameForNode[node] = stringID;
}
int getNodeLevel(const Scene &scene, int n);
bool recalculateGlobalTransforms(Scene &scene);
void loadScene(const char *fileName, Scene &scene);
void saveScene(const char *fileName, const Scene &scene);
void dumpTransforms(const char *fileName, const Scene &scene);
void printChangedNodes(const Scene &scene);
void dumpSceneToDot(const char *fileName, const Scene &scene,
int *visited = nullptr);
void mergeScenes(Scene &scene, const std::vector<Scene *> &scenes,
const std::vector<glm::mat4> &rootTransforms,
const std::vector<uint32_t> &meshCounts,
bool mergeMeshes = true, bool mergeMaterials = true);
// Delete a collection of nodes from a scenegraph
void deleteSceneNodes(Scene &scene, const std::vector<uint32_t> &nodesToDelete);

342
shared/Scene/VtxData.cpp Normal file
View file

@ -0,0 +1,342 @@
#include "shared/Scene/VtxData.h"
#include <algorithm>
#include <assert.h>
#include <stdio.h>
bool isMeshDataValid(const char* fileName)
{
FILE* f = fopen(fileName, "rb");
if (!f)
return false;
SCOPE_EXIT
{
fclose(f);
};
MeshFileHeader header;
if (fread(&header, 1, sizeof(header), f) != sizeof(header))
return false;
if (fseek(f, sizeof(Mesh) * header.meshCount, SEEK_CUR))
return false;
if (fseek(f, sizeof(BoundingBox) * header.meshCount, SEEK_CUR))
return false;
if (fseek(f, header.indexDataSize, SEEK_CUR))
return false;
if (fseek(f, header.vertexDataSize, SEEK_CUR))
return false;
return true;
}
bool isMeshHierarchyValid(const char* fileName)
{
FILE* f = fopen(fileName, "rb");
if (!f)
return false;
SCOPE_EXIT
{
fclose(f);
};
return true;
}
bool isMeshMaterialsValid(const char* fileName)
{
FILE* f = fopen(fileName, "rb");
if (!f)
return false;
SCOPE_EXIT
{
fclose(f);
};
uint64_t numMaterials = 0;
uint64_t materialsSize = 0;
if (fread(&numMaterials, 1, sizeof(numMaterials), f) != sizeof(numMaterials))
return false;
if (fread(&materialsSize, 1, sizeof(materialsSize), f) != sizeof(materialsSize))
return false;
if (numMaterials * sizeof(Material) != materialsSize)
return false;
return true;
}
MeshFileHeader loadMeshData(const char* meshFile, MeshData& out)
{
FILE* f = fopen(meshFile, "rb");
assert(f);
if (!f) {
printf("Cannot open '%s'.\n", meshFile);
assert(false);
exit(EXIT_FAILURE);
}
SCOPE_EXIT
{
fclose(f);
};
MeshFileHeader header;
if (fread(&header, 1, sizeof(header), f) != sizeof(header)) {
printf("Unable to read mesh file header.\n");
assert(false);
exit(EXIT_FAILURE);
}
if (fread(&out.streams, 1, sizeof(out.streams), f) != sizeof(out.streams)) {
printf("Unable to read vertex streams description.\n");
assert(false);
exit(EXIT_FAILURE);
}
out.meshes.resize(header.meshCount);
if (fread(out.meshes.data(), sizeof(Mesh), header.meshCount, f) != header.meshCount) {
printf("Could not read mesh descriptors.\n");
assert(false);
exit(EXIT_FAILURE);
}
out.boxes.resize(header.meshCount);
if (fread(out.boxes.data(), sizeof(BoundingBox), header.meshCount, f) != header.meshCount) {
printf("Could not read bounding boxes.\n");
assert(false);
exit(EXIT_FAILURE);
}
out.indexData.resize(header.indexDataSize / sizeof(uint32_t));
out.vertexData.resize(header.vertexDataSize);
if (fread(out.indexData.data(), 1, header.indexDataSize, f) != header.indexDataSize) {
printf("Unable to read index data.\n");
assert(false);
exit(EXIT_FAILURE);
}
if (fread(out.vertexData.data(), 1, header.vertexDataSize, f) != header.vertexDataSize) {
printf("Unable to read vertex data.\n");
assert(false);
exit(EXIT_FAILURE);
}
return header;
}
void loadMeshDataMaterials(const char* fileName, MeshData& out)
{
FILE* f = fopen(fileName, "rb");
if (!f) {
printf("Cannot open '%s'.\n", fileName);
assert(false);
exit(EXIT_FAILURE);
}
uint64_t numMaterials = 0;
uint64_t materialsSize = 0;
if (fread(&numMaterials, 1, sizeof(numMaterials), f) != sizeof(numMaterials)) {
printf("Unable to read numMaterials.\n");
assert(false);
exit(EXIT_FAILURE);
}
if (fread(&materialsSize, 1, sizeof(materialsSize), f) != sizeof(materialsSize)) {
printf("Unable to read materialsSize.\n");
assert(false);
exit(EXIT_FAILURE);
}
if (numMaterials * sizeof(Material) != materialsSize) {
printf("Corrupted material file '%s'.\n", fileName);
assert(false);
exit(EXIT_FAILURE);
}
out.materials.resize(numMaterials);
if (fread(out.materials.data(), 1, materialsSize, f) != materialsSize) {
printf("Unable to read material data.\n");
assert(false);
exit(EXIT_FAILURE);
}
loadStringList(f, out.textureFiles);
fclose(f);
}
void saveMeshData(const char* fileName, const MeshData& m)
{
FILE* f = fopen(fileName, "wb");
if (!f) {
printf("Error opening file '%s' for writing.\n", fileName);
assert(false);
exit(EXIT_FAILURE);
}
const MeshFileHeader header = {
.meshCount = (uint32_t)m.meshes.size(),
.indexDataSize = (uint32_t)(m.indexData.size() * sizeof(uint32_t)),
.vertexDataSize = (uint32_t)(m.vertexData.size()),
};
fwrite(&header, 1, sizeof(header), f);
fwrite(&m.streams, 1, sizeof(m.streams), f);
fwrite(m.meshes.data(), sizeof(Mesh), header.meshCount, f);
fwrite(m.boxes.data(), sizeof(BoundingBox), header.meshCount, f);
fwrite(m.indexData.data(), 1, header.indexDataSize, f);
fwrite(m.vertexData.data(), 1, header.vertexDataSize, f);
fclose(f);
}
void saveMeshDataMaterials(const char* fileName, const MeshData& m)
{
FILE* f = fopen(fileName, "wb");
if (!f) {
printf("Error opening file '%s' for writing.\n", fileName);
assert(false);
exit(EXIT_FAILURE);
}
const uint64_t numMaterials = m.materials.size();
const uint64_t materialsSize = m.materials.size() * sizeof(Material);
fwrite(&numMaterials, 1, sizeof(numMaterials), f);
fwrite(&materialsSize, 1, sizeof(materialsSize), f);
fwrite(m.materials.data(), sizeof(Material), numMaterials, f);
saveStringList(f, m.textureFiles);
fclose(f);
}
void saveBoundingBoxes(const char* fileName, const std::vector<BoundingBox>& boxes)
{
FILE* f = fopen(fileName, "wb");
if (!f) {
printf("Error opening bounding boxes file '%s' for writing.\n", fileName);
assert(false);
exit(EXIT_FAILURE);
}
const uint32_t sz = (uint32_t)boxes.size();
fwrite(&sz, 1, sizeof(sz), f);
fwrite(boxes.data(), sz, sizeof(BoundingBox), f);
fclose(f);
}
void loadBoundingBoxes(const char* fileName, std::vector<BoundingBox>& boxes)
{
FILE* f = fopen(fileName, "rb");
if (!f) {
printf("Error opening bounding boxes file '%s'\n", fileName);
assert(false);
exit(EXIT_FAILURE);
}
uint32_t sz;
fread(&sz, 1, sizeof(sz), f);
// TODO: check file size, divide by bounding box size
boxes.resize(sz);
fread(boxes.data(), sz, sizeof(BoundingBox), f);
fclose(f);
}
// combine a collection of meshes into a single MeshData container
MeshFileHeader mergeMeshData(MeshData& m, const std::vector<MeshData*> md)
{
uint32_t numTotalVertices = 0;
uint32_t numTotalIndices = 0;
if (!md.empty()) {
m.streams = md[0]->streams;
}
const uint32_t vertexSize = m.streams.getVertexSize();
uint32_t offset = 0;
uint32_t mtlOffset = 0;
for (const MeshData* i : md) {
LVK_ASSERT(m.streams == i->streams);
mergeVectors(m.indexData, i->indexData);
mergeVectors(m.vertexData, i->vertexData);
mergeVectors(m.meshes, i->meshes);
mergeVectors(m.boxes, i->boxes);
for (size_t j = 0; j != i->meshes.size(); j++) {
// m.vertexCount, m.lodCount and m.streamCount do not change
// m.vertexOffset also does not change, because vertex offsets are local (i.e., baked into the indices)
m.meshes[offset + j].indexOffset += numTotalIndices;
m.meshes[offset + j].materialID += mtlOffset;
}
// shift individual indices
for (size_t j = 0; j != i->indexData.size(); j++) {
m.indexData[numTotalIndices + j] += numTotalVertices;
}
offset += (uint32_t)i->meshes.size();
mtlOffset += (uint32_t)i->materials.size();
numTotalIndices += (uint32_t)i->indexData.size();
numTotalVertices += (uint32_t)i->vertexData.size() / vertexSize;
}
return MeshFileHeader{
.magicValue = 0x12345678,
.meshCount = (uint32_t)offset,
.indexDataSize = static_cast<uint32_t>(numTotalIndices * sizeof(uint32_t)),
.vertexDataSize = static_cast<uint32_t>(m.vertexData.size()),
};
}
void recalculateBoundingBoxes(MeshData& m)
{
LVK_ASSERT(m.streams.attributes[0].format == lvk::VertexFormat::Float3);
const uint32_t stride = m.streams.getVertexSize();
m.boxes.clear();
m.boxes.reserve(m.meshes.size());
for (const Mesh& mesh : m.meshes) {
const uint32_t numIndices = mesh.getLODIndicesCount(0);
glm::vec3 vmin(std::numeric_limits<float>::max());
glm::vec3 vmax(std::numeric_limits<float>::lowest());
for (uint32_t i = 0; i != numIndices; i++) {
const uint32_t vtxOffset = m.indexData[mesh.indexOffset + i] + mesh.vertexOffset;
const float* vf = (const float*)&m.vertexData[vtxOffset * stride];
vmin = glm::min(vmin, vec3(vf[0], vf[1], vf[2]));
vmax = glm::max(vmax, vec3(vf[0], vf[1], vf[2]));
}
m.boxes.emplace_back(vmin, vmax);
}
}

111
shared/Scene/VtxData.h Normal file
View file

@ -0,0 +1,111 @@
#pragma once
#include <stdint.h>
#include <glm/glm.hpp>
#include "shared/Utils.h"
#include "shared/UtilsMath.h"
constexpr const uint32_t kMaxLODs = 7;
// All offsets are relative to the beginning of the data block (excluding headers with a Mesh list)
struct Mesh final {
// Number of LODs in this mesh. Strictly less than MAX_LODS, last LOD offset is used as a marker only
uint32_t lodCount = 1;
// The total count of all previous vertices in this mesh file
uint32_t indexOffset = 0;
uint32_t vertexOffset = 0;
// Vertex count (for all LODs)
uint32_t vertexCount = 0;
// Offsets to LOD indices data. The last offset is used as a marker to calculate the size
uint32_t lodOffset[kMaxLODs + 1] = { 0 };
uint32_t materialID = 0;
inline uint32_t getLODIndicesCount(uint32_t lod) const { return lod < lodCount ? lodOffset[lod + 1] - lodOffset[lod] : 0; }
// Any additional information, such as mesh name, can be added here...
};
struct MeshFileHeader {
// Unique 64-bit value to check integrity of the file
uint32_t magicValue = 0x12345678;
// Number of mesh descriptors following this header
uint32_t meshCount = 0;
// How much space index data takes in bytes
uint32_t indexDataSize = 0;
// How much space vertex data takes in bytes
uint32_t vertexDataSize = 0;
// According to your needs, you may add additional metadata fields...
};
enum MaterialFlags {
sMaterialFlags_CastShadow = 0x1,
sMaterialFlags_ReceiveShadow = 0x2,
sMaterialFlags_Transparent = 0x4,
};
struct Material {
vec4 emissiveFactor = vec4(0.0f, 0.0f, 0.0f, 0.0f);
vec4 baseColorFactor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
float roughness = 1.0f;
float transparencyFactor = 1.0f;
float alphaTest = 0.0f;
float metallicFactor = 0.0f;
// index into MeshData::textureFiles
int baseColorTexture = -1;
int emissiveTexture = -1;
int normalTexture = -1;
int opacityTexture = -1;
uint32_t flags = sMaterialFlags_CastShadow | sMaterialFlags_ReceiveShadow;
};
struct MeshData {
lvk::VertexInput streams = {};
std::vector<uint32_t> indexData;
std::vector<uint8_t> vertexData;
std::vector<Mesh> meshes;
std::vector<BoundingBox> boxes;
std::vector<Material> materials;
std::vector<std::string> textureFiles;
MeshFileHeader getMeshFileHeader() const
{
return {
.meshCount = (uint32_t)meshes.size(),
.indexDataSize = (uint32_t)(indexData.size() * sizeof(uint32_t)),
.vertexDataSize = (uint32_t)vertexData.size(),
};
}
};
static_assert(sizeof(BoundingBox) == sizeof(float) * 6);
bool isMeshDataValid(const char* fileName);
bool isMeshMaterialsValid(const char* fileName);
bool isMeshHierarchyValid(const char* fileName);
MeshFileHeader loadMeshData(const char* meshFile, MeshData& out);
void loadMeshDataMaterials(const char* meshFile, MeshData& out);
void saveMeshData(const char* fileName, const MeshData& m);
void saveMeshDataMaterials(const char* fileName, const MeshData& m);
void recalculateBoundingBoxes(MeshData& m);
// combine a list of meshes to a single mesh container
MeshFileHeader mergeMeshData(MeshData& m, const std::vector<MeshData*> md);
// use to write values into MeshData::vertexData
template <typename T> inline void put(std::vector<uint8_t>& v, const T& value)
{
const size_t pos = v.size();
v.resize(v.size() + sizeof(value));
memcpy(v.data() + pos, &value, sizeof(value));
}

63
shared/Tonemap.h Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include <glm/ext.hpp>
#include <glm/glm.hpp>
enum ToneMappingMode {
ToneMapping_None = 0,
ToneMapping_Reinhard = 1,
ToneMapping_Uchimura = 2,
ToneMapping_KhronosPBR = 3,
};
// Uchimura 2017, "HDR theory and practice"
// Math: https://www.desmos.com/calculator/gslcdxvipg
// Source: https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp
float uchimura(float x, float P, float a, float m, float l, float c, float b)
{
float l0 = ((P - m) * l) / a;
float L0 = m - m / a;
float L1 = m + (1.0f - m) / a;
float S0 = m + l0;
float S1 = m + a * l0;
float C2 = (a * P) / (P - S1);
float CP = -C2 / P;
float w0 = float(1.0f - glm::smoothstep(0.0f, m, x));
float w2 = float(glm::step(m + l0, x));
float w1 = float(1.0f - w0 - w2);
float T = float(m * pow(x / m, float(c)) + b);
float S = float(P - (P - S1) * exp(CP * (x - S0)));
float L = float(m + a * (x - m));
return T * w0 + L * w1 + S * w2;
}
float reinhard2(float v, float maxWhite)
{
return v * (1.0f + (v / (maxWhite * maxWhite))) / (1.0f + v);
}
// Khronos PBR Neutral Tone Mapper
// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/README.md#pbr-neutral-specification
// https://github.com/KhronosGroup/ToneMapping/blob/main/PBR_Neutral/pbrNeutral.glsl
float PBRNeutralToneMapping(float color, float startCompression, float desaturation)
{
startCompression -= 0.04f;
float x = color;
float offset = x < 0.08f ? x - 6.25f * x * x : 0.04f;
color -= offset;
float peak = color;
if (peak < startCompression)
return color;
const float d = 1. - startCompression;
float newPeak = 1. - d * d / (peak + d - startCompression);
color *= newPeak / peak;
float g = 1.0f - 1.0f / (desaturation * (peak - newPeak) + 1.0f);
return glm::mix(color, newPeak, g);
}

126
shared/Trackball.h Normal file
View file

@ -0,0 +1,126 @@
/*
* Copyright (C) 2013-2015 Sergey Kosarevsky (sk@linderdaum.com)
* Copyright (C) 2013-2015 Viktor Latypov (vl@linderdaum.com)
* Based on Linderdaum Engine http://www.linderdaum.com
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must display the names 'Sergey Kosarevsky' and
* 'Viktor Latypov' in the credits of the application, if such credits exist.
* The authors of this work must be notified via email (sk@linderdaum.com) in
* this case of redistribution.
*
* 3. Neither the name of copyright holders nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS
* IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL COPYRIGHT HOLDERS OR CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
#pragma once
#include <numeric>
#include <glm/glm.hpp>
#include <glm/ext.hpp>
/// Virtual trackball for user interaction with rotations
class VirtualTrackball
{
public:
VirtualTrackball() = default;
/**
Get rotation matrix for new mouse point
**/
glm::mat4 dragTo(glm::vec2 screenPoint, float speed, bool keyPressed)
{
if (keyPressed && !isDraggingActive_)
{
startDragging(screenPoint);
isDraggingActive_ = keyPressed;
return glm::mat4(1.0f);
}
isDraggingActive_ = keyPressed;
if (!keyPressed) return glm::mat4(1.0f);
pointCur_ = projectOnSphere(screenPoint);
const glm::vec3 direction = pointCur_ - pointPrev_;
const float shift = glm::length(direction);
glm::mat4 rotMatrix = glm::mat4(1.0f);
if (shift > std::numeric_limits<float>::epsilon())
{
const glm::vec3 axis = glm::cross( pointPrev_, pointCur_);
rotMatrix = glm::rotate(glm::mat4(1.0f), shift * speed, axis);
}
rotationDelta_ = rotMatrix;
return rotMatrix;
}
const glm::mat4& getRotationDelta() const
{
return rotationDelta_;
};
/**
Get current rotation matrix
**/
glm::mat4 getRotationMatrix() const
{
return rotation_ * rotationDelta_;
}
private:
void startDragging(glm::vec2 screenPoint)
{
rotation_ = rotation_ * rotationDelta_;
rotationDelta_ = glm::mat4(1.0f);
pointCur_ = projectOnSphere(screenPoint);
pointPrev_ = pointCur_;
}
glm::vec3 projectOnSphere(glm::vec2 ScreenPoint)
{
// convert to -1.0...1.0 range
glm::vec3 proj(
+(2.0f * ScreenPoint.x - 1.0f),
-(2.0f * ScreenPoint.y - 1.0f),
0.0f
);
const float Length = std::min(glm::length(proj), 1.0f);
proj.z = sqrtf(1.001f - Length * Length);
return glm::normalize(proj);
}
glm::vec3 pointCur_ = glm::vec3(0.0f);
glm::vec3 pointPrev_ = glm::vec3(0.0f);
glm::mat4 rotation_ = glm::mat4(1.0f);
glm::mat4 rotationDelta_ = glm::mat4(1.0f);
bool isDraggingActive_ = false;
};

286
shared/Utils.cpp Normal file
View file

@ -0,0 +1,286 @@
#include <string.h>
#include <string>
#if !defined(__APPLE__)
#include <malloc.h>
#endif
#include "Utils.h"
#include <stb/stb_image.h>
#include <ktx.h>
#include <ktx-software/lib/gl_format.h>
#include <unordered_map>
// lvk::ShaderModuleHandle -> GLSL source code
std::unordered_map<uint32_t, std::string> debugGLSLSourceCode;
bool endsWith(const char* s, const char* part)
{
const size_t sLength = strlen(s);
const size_t partLength = strlen(part);
return sLength < partLength ? false : strcmp(s + sLength - partLength, part) == 0;
}
std::string readShaderFile(const char* fileName)
{
FILE* file = fopen(fileName, "r");
if (!file) {
LLOGW("I/O error. Cannot open shader file '%s'\n", fileName);
return std::string();
}
fseek(file, 0L, SEEK_END);
const size_t bytesinfile = ftell(file);
fseek(file, 0L, SEEK_SET);
char* buffer = (char*)alloca(bytesinfile + 1);
const size_t bytesread = fread(buffer, 1, bytesinfile, file);
fclose(file);
buffer[bytesread] = 0;
static constexpr unsigned char BOM[] = { 0xEF, 0xBB, 0xBF };
if (bytesread > 3) {
if (!memcmp(buffer, BOM, 3))
memset(buffer, ' ', 3);
}
std::string code(buffer);
while (code.find("#include ") != code.npos) {
const auto pos = code.find("#include ");
const auto p1 = code.find('<', pos);
const auto p2 = code.find('>', pos);
if (p1 == code.npos || p2 == code.npos || p2 <= p1) {
LLOGW("Error while loading shader program: %s\n", code.c_str());
return std::string();
}
const std::string name = code.substr(p1 + 1, p2 - p1 - 1);
const std::string include = readShaderFile(name.c_str());
code.replace(pos, p2 - pos + 1, include.c_str());
}
return code;
}
VkShaderStageFlagBits vkShaderStageFromFileName(const char* fileName)
{
if (endsWith(fileName, ".vert"))
return VK_SHADER_STAGE_VERTEX_BIT;
if (endsWith(fileName, ".frag"))
return VK_SHADER_STAGE_FRAGMENT_BIT;
if (endsWith(fileName, ".geom"))
return VK_SHADER_STAGE_GEOMETRY_BIT;
if (endsWith(fileName, ".comp"))
return VK_SHADER_STAGE_COMPUTE_BIT;
if (endsWith(fileName, ".tesc"))
return VK_SHADER_STAGE_TESSELLATION_CONTROL_BIT;
if (endsWith(fileName, ".tese"))
return VK_SHADER_STAGE_TESSELLATION_EVALUATION_BIT;
return VK_SHADER_STAGE_VERTEX_BIT;
}
lvk::ShaderStage lvkShaderStageFromFileName(const char* fileName)
{
if (endsWith(fileName, ".vert"))
return lvk::Stage_Vert;
if (endsWith(fileName, ".frag"))
return lvk::Stage_Frag;
if (endsWith(fileName, ".geom"))
return lvk::Stage_Geom;
if (endsWith(fileName, ".comp"))
return lvk::Stage_Comp;
if (endsWith(fileName, ".tesc"))
return lvk::Stage_Tesc;
if (endsWith(fileName, ".tese"))
return lvk::Stage_Tese;
return lvk::Stage_Vert;
}
lvk::Holder<lvk::ShaderModuleHandle> loadShaderModule(const std::unique_ptr<lvk::IContext>& ctx, const char* fileName) {
const std::string code = readShaderFile(fileName);
const lvk::ShaderStage stage = lvkShaderStageFromFileName(fileName);
if (code.empty()) {
return {};
}
lvk::Result res;
lvk::Holder<lvk::ShaderModuleHandle> handle =
ctx->createShaderModule({ code.c_str(), stage, (std::string("Shader Module: ") + fileName).c_str() }, &res);
if (!res.isOk()) {
return {};
}
debugGLSLSourceCode[handle.index()] = code;
return handle;
}
lvk::Holder<lvk::TextureHandle> loadTexture(
const std::unique_ptr<lvk::IContext>& ctx, const char* fileName, lvk::TextureType textureType, bool sRGB)
{
const bool isKTX = endsWith(fileName, ".ktx") || endsWith(fileName, ".KTX");
lvk::Result result;
lvk::Holder<lvk::TextureHandle> texture;
if (isKTX) {
ktxTexture1* ktxTex = nullptr;
if (!LVK_VERIFY(ktxTexture1_CreateFromNamedFile(fileName, KTX_TEXTURE_CREATE_LOAD_IMAGE_DATA_BIT, &ktxTex) == KTX_SUCCESS)) {
LLOGW("Failed to load %s\n", fileName);
assert(0);
return {};
}
SCOPE_EXIT
{
ktxTexture_Destroy(ktxTexture(ktxTex));
};
lvk::Result result;
const lvk::Format format = [](uint32_t glInternalFormat) {
switch (glInternalFormat) {
case GL_COMPRESSED_RGBA_BPTC_UNORM:
return lvk::Format_BC7_RGBA;
case GL_RGBA8:
return lvk::Format_RGBA_UN8;
case GL_RG16F:
return lvk::Format_RG_F16;
case GL_RGBA16F:
return lvk::Format_RGBA_F16;
case GL_RGBA32F:
return lvk::Format_RGBA_F32;
default:
LLOGW("Unsupported pixel format (%u)\n", glInternalFormat);
assert(0);
}
return lvk::Format_Invalid;
}(ktxTex->glInternalformat);
texture = ctx->createTexture(
{
.type = textureType,
.format = format,
.dimensions = {ktxTex->baseWidth, ktxTex->baseHeight, 1},
.usage = lvk::TextureUsageBits_Sampled,
.numMipLevels = ktxTex->numLevels,
.data = ktxTex->pData,
.dataNumMipLevels = ktxTex->numLevels,
.debugName = fileName,
},
fileName, &result);
} else {
LVK_ASSERT(textureType == lvk::TextureType_2D);
int w, h, comp;
const uint8_t* img = stbi_load(fileName, &w, &h, &comp, 4);
SCOPE_EXIT
{
if (img)
stbi_image_free((void*)img);
};
if (!img) {
printf("Unable to load %s. File not found.\n", fileName);
return {};
};
texture = ctx->createTexture(
{
.type = lvk::TextureType_2D,
.format = sRGB ? lvk::Format_RGBA_SRGB8 : lvk::Format_RGBA_UN8,
.dimensions = {(uint32_t)w, (uint32_t)h},
.usage = lvk::TextureUsageBits_Sampled,
.data = img,
.debugName = fileName,
},
fileName, &result);
}
if (!result.isOk()) {
printf("Unable to load %s. Reason: %s\n", fileName, result.message);
}
return texture;
}
void saveStringList(FILE* f, const std::vector<std::string>& lines)
{
uint32_t sz = (uint32_t)lines.size();
fwrite(&sz, sizeof(uint32_t), 1, f);
for (const std::string& s : lines) {
sz = (uint32_t)s.length();
fwrite(&sz, sizeof(uint32_t), 1, f);
fwrite(s.c_str(), sz + 1, 1, f);
}
}
void loadStringList(FILE* f, std::vector<std::string>& lines)
{
{
uint32_t sz = 0;
fread(&sz, sizeof(uint32_t), 1, f);
lines.resize(sz);
}
std::vector<char> inBytes;
for (std::string& s : lines) {
uint32_t sz = 0;
fread(&sz, sizeof(uint32_t), 1, f);
inBytes.resize(sz + 1);
fread(inBytes.data(), sz + 1, 1, f);
s = std::string(inBytes.data());
}
}
int addUnique(std::vector<std::string>& files, const std::string& file)
{
if (file.empty())
return -1;
const auto i = std::find(std::begin(files), std::end(files), file);
if (i != files.end())
return (int)std::distance(files.begin(), i);
files.push_back(file);
return (int)files.size() - 1;
}
std::string replaceAll(const std::string& str, const std::string& oldSubStr, const std::string& newSubStr)
{
std::string result = str;
for (size_t p = result.find(oldSubStr); p != std::string::npos; p = result.find(oldSubStr))
result.replace(p, oldSubStr.length(), newSubStr);
return result;
}
// Convert 8-bit ASCII string to upper case
std::string lowercaseString(const std::string& s)
{
std::string out(s.length(), ' ');
std::transform(s.begin(), s.end(), out.begin(), tolower);
return out;
}

54
shared/Utils.h Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include <algorithm>
#include <memory>
#include <string.h>
#include <string>
#include <vector>
#if !defined(__APPLE__)
#include <malloc.h>
#endif
#include <glslang/Include/glslang_c_interface.h>
#include <ldrutils/lutils/ScopeExit.h>
#include <lvk/LVK.h>
#include <lvk/vulkan/VulkanUtils.h>
bool endsWith(const char* s, const char* part);
std::string readShaderFile(const char* fileName);
VkShaderStageFlagBits vkShaderStageFromFileName(const char* fileName);
lvk::Holder<lvk::ShaderModuleHandle> loadShaderModule(const std::unique_ptr<lvk::IContext>& ctx, const char* fileName);
lvk::Holder<lvk::TextureHandle> loadTexture(
const std::unique_ptr<lvk::IContext>& ctx, const char* fileName, lvk::TextureType textureType = lvk::TextureType_2D, bool sRGB = false);
template <typename T> inline void mergeVectors(std::vector<T>& v1, const std::vector<T>& v2)
{
v1.insert(v1.end(), v2.begin(), v2.end());
}
// From https://stackoverflow.com/a/64152990/1182653
// Delete a list of items from std::vector with indices in 'selection'
// e.g., eraseSelected({1, 2, 3, 4, 5}, {1, 3}) -> {1, 3, 5}
// ^ ^ 2 and 4 get deleted
template <typename T, typename Index = int> inline void eraseSelected(std::vector<T>& v, const std::vector<Index>& selection)
{
// cut off the elements moved to the end of the vector by std::stable_partition
v.resize(std::distance(
v.begin(),
// std::stable_partition moves any element whose index is in 'selection' to the end
std::stable_partition(v.begin(), v.end(), [&selection, &v](const T& item) {
return !std::binary_search(
selection.begin(), selection.end(),
// std::distance(std::find(v.begin(), v.end(), item), v.begin()) - if you don't like the pointer arithmetic below
static_cast<Index>(static_cast<const T*>(&item) - &v[0]));
})));
}
void saveStringList(FILE* f, const std::vector<std::string>& lines);
void loadStringList(FILE* f, std::vector<std::string>& lines);
int addUnique(std::vector<std::string>& files, const std::string& file);
std::string replaceAll(const std::string& str, const std::string& oldSubStr, const std::string& newSubStr);
std::string lowercaseString(const std::string& s); // convert 8-bit ASCII string to upper case

319
shared/UtilsAnim.cpp Normal file
View file

@ -0,0 +1,319 @@
#include "UtilsAnim.h"
#include "UtilsGLTF.h"
#include "VulkanApp.h"
#include <assimp/GltfMaterial.h>
#include <assimp/cimport.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <assimp/types.h>
#include <cmath>
using glm::quat;
AnimationChannel initChannel(const aiNodeAnim* anim)
{
AnimationChannel channel;
channel.pos.resize(anim->mNumPositionKeys);
for (uint32_t i = 0; i < anim->mNumPositionKeys; ++i) {
channel.pos[i] = { .pos = aiVector3DToVec3(anim->mPositionKeys[i].mValue), .time = (float)anim->mPositionKeys[i].mTime };
}
channel.rot.resize(anim->mNumRotationKeys);
for (uint32_t i = 0; i < anim->mNumRotationKeys; ++i) {
channel.rot[i] = { .rot = aiQuaternionToQuat(anim->mRotationKeys[i].mValue), .time = (float)anim->mRotationKeys[i].mTime };
}
channel.scale.resize(anim->mNumScalingKeys);
for (uint32_t i = 0; i < anim->mNumScalingKeys; ++i) {
channel.scale[i] = { .scale = aiVector3DToVec3(anim->mScalingKeys[i].mValue), .time = (float)anim->mScalingKeys[i].mTime };
}
return channel;
}
template <typename T> uint32_t getTimeIndex(const std::vector<T>& t, float time)
{
return std::max(
0,
(int)std::distance(t.begin(), std::lower_bound(t.begin(), t.end(), time, [&](const T& lhs, float rhs) { return lhs.time < rhs; })) -
1);
}
float interpolationVal(float lastTimeStamp, float nextTimeStamp, float animationTime)
{
return (animationTime - lastTimeStamp) / (nextTimeStamp - lastTimeStamp);
}
vec3 interpolatePosition(const AnimationChannel& channel, float time)
{
if (channel.pos.size() == 1)
return channel.pos[0].pos;
uint32_t start = getTimeIndex<>(channel.pos, time);
uint32_t end = start + 1;
float mix = interpolationVal(channel.pos[start].time, channel.pos[end].time, time);
return glm::mix(channel.pos[start].pos, channel.pos[end].pos, mix);
}
glm::quat interpolateRotation(const AnimationChannel& channel, float time)
{
if (channel.rot.size() == 1)
return channel.rot[0].rot;
uint32_t start = getTimeIndex<>(channel.rot, time);
uint32_t end = start + 1;
float mix = interpolationVal(channel.rot[start].time, channel.rot[end].time, time);
return glm::slerp(channel.rot[start].rot, channel.rot[end].rot, mix);
}
vec3 interpolateScaling(const AnimationChannel& channel, float time)
{
if (channel.scale.size() == 1)
return channel.scale[0].scale;
uint32_t start = getTimeIndex<>(channel.scale, time);
uint32_t end = start + 1;
float coef = interpolationVal(channel.scale[start].time, channel.scale[end].time, time);
return glm::mix(channel.scale[start].scale, channel.scale[end].scale, coef);
}
mat4 animationTransform(const AnimationChannel& channel, float time)
{
mat4 translation = glm::translate(mat4(1.0f), interpolatePosition(channel, time));
mat4 rotation = glm::toMat4(glm::normalize(interpolateRotation(channel, time)));
mat4 scale = glm::scale(mat4(1.0f), interpolateScaling(channel, time));
return translation * rotation * scale;
}
mat4 animationTransformBlending(const AnimationChannel& channel1, float time1, const AnimationChannel& channel2, float time2, float weight)
{
mat4 trans1 = glm::translate(mat4(1.0f), interpolatePosition(channel1, time1));
mat4 trans2 = glm::translate(mat4(1.0f), interpolatePosition(channel2, time2));
mat4 translation = glm::mix(trans1, trans2, weight);
quat rot1 = interpolateRotation(channel1, time1);
quat rot2 = interpolateRotation(channel2, time2);
mat4 rotation = glm::toMat4(glm::normalize(glm::slerp(rot1, rot2, weight)));
vec3 scl1 = interpolateScaling(channel1, time1);
vec3 scl2 = interpolateScaling(channel2, time2);
mat4 scale = glm::scale(mat4(1.0f), glm::mix(scl1, scl2, weight));
return translation * rotation * scale;
}
MorphState morphTransform(const MorphTarget& target, const MorphingChannel& channel, float time)
{
MorphState ms;
ms.meshId = target.meshId;
float mix = 0.0f;
int start = 0;
int end = 0;
if (channel.key.size() > 0) {
start = getTimeIndex(channel.key, time);
end = start + 1;
mix = interpolationVal(channel.key[start].time, channel.key[end].time, time);
}
for (uint32_t i = 0; i < std::min((uint32_t)target.offset.size(), (uint32_t)MAX_MORPH_WEIGHTS); ++i) {
ms.morphTarget[i] = target.offset[channel.key[start].mesh[i]];
ms.weights[i] = glm::mix(channel.key[start].weight[i], channel.key[end].weight[i], mix);
}
return ms;
}
void initAnimations(GLTFContext& glTF, const aiScene* scene)
{
glTF.animations.resize(scene->mNumAnimations);
for (uint32_t i = 0; i < scene->mNumAnimations; ++i) {
Animation& anim = glTF.animations[i];
anim.name = scene->mAnimations[i]->mName.C_Str();
anim.duration = scene->mAnimations[i]->mDuration;
anim.ticksPerSecond = scene->mAnimations[i]->mTicksPerSecond;
for (uint32_t c = 0; c < scene->mAnimations[i]->mNumChannels; c++) {
const aiNodeAnim* channel = scene->mAnimations[i]->mChannels[c];
const char* boneName = channel->mNodeName.data;
uint32_t boneId = glTF.bonesByName[boneName].boneId;
if (boneId == ~0u) {
for (const GLTFNode& node : glTF.nodesStorage) {
if (node.name != boneName)
continue;
boneId = node.modelMtxId;
glTF.bonesByName[boneName] = {
.boneId = boneId,
.transform = glTF.hasBones ? glm::inverse(node.transform) : mat4(1),
};
break;
}
}
assert(boneId != ~0u);
anim.channels[boneId] = initChannel(channel);
}
const uint32_t numMorphTargetChannels = scene->mAnimations[i]->mNumMorphMeshChannels;
anim.morphChannels.resize(numMorphTargetChannels);
for (uint32_t c = 0; c < numMorphTargetChannels; c++) {
const aiMeshMorphAnim* channel = scene->mAnimations[i]->mMorphMeshChannels[c];
MorphingChannel& morphChannel = anim.morphChannels[c];
morphChannel.name = channel->mName.C_Str();
morphChannel.key.resize(channel->mNumKeys);
for (uint32_t k = 0; k < channel->mNumKeys; ++k) {
MorphingChannelKey& key = morphChannel.key[k];
key.time = channel->mKeys[k].mTime;
for (uint32_t v = 0; v < std::min((uint32_t)MAX_MORPH_WEIGHTS, channel->mKeys[k].mNumValuesAndWeights); ++v) {
key.mesh[v] = channel->mKeys[k].mValues[v];
key.weight[v] = channel->mKeys[k].mWeights[v];
}
}
}
}
}
void updateAnimation(GLTFContext& glTF, AnimationState& anim, float dt)
{
if (!anim.active || (anim.animId == ~0u)) {
glTF.morphing = false;
glTF.skinning = false;
return;
}
const Animation& activeAnim = glTF.animations[anim.animId];
anim.currentTime += activeAnim.ticksPerSecond * dt;
if (anim.playOnce && anim.currentTime > activeAnim.duration) {
anim.currentTime = activeAnim.duration;
anim.active = false;
} else
anim.currentTime = fmodf(anim.currentTime, activeAnim.duration);
// Apply animations
std::function<void(GLTFNodeRef gltfNode, const mat4& parentTransform)> traverseTree = [&](GLTFNodeRef gltfNode,
const mat4& parentTransform) {
const GLTFBone& bone = glTF.bonesByName[glTF.nodesStorage[gltfNode].name];
const uint32_t boneId = bone.boneId;
if (boneId != ~0u) {
assert(boneId == glTF.nodesStorage[gltfNode].modelMtxId);
auto channel = activeAnim.channels.find(boneId);
const bool hasActiveChannel = channel != activeAnim.channels.end();
glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId] =
parentTransform *
(hasActiveChannel ? animationTransform(channel->second, anim.currentTime) : glTF.nodesStorage[gltfNode].transform);
glTF.skinning = true;
} else {
glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId] = parentTransform * glTF.nodesStorage[gltfNode].transform;
}
for (uint32_t i = 0; i < glTF.nodesStorage[gltfNode].children.size(); i++) {
const GLTFNodeRef child = glTF.nodesStorage[gltfNode].children[i];
traverseTree(child, glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId]);
}
};
traverseTree(glTF.root, mat4(1.0f));
for (const std::pair<std::string, GLTFBone>& b : glTF.bonesByName) {
if (b.second.boneId != ~0u) {
glTF.matrices[b.second.boneId] = glTF.matrices[b.second.boneId] * b.second.transform;
}
}
glTF.morphStates.resize(glTF.meshesStorage.size());
// update morphing
if (glTF.enableMorphing) {
if (!activeAnim.morphChannels.empty()) {
for (size_t i = 0; i < activeAnim.morphChannels.size(); ++i) {
const MorphingChannel& channel = activeAnim.morphChannels[i];
const uint32_t meshId = glTF.meshesRemap[channel.name];
const MorphTarget& morphTarget = glTF.morphTargets[meshId];
if (morphTarget.meshId != ~0u) {
glTF.morphStates[morphTarget.meshId] = morphTransform(morphTarget, channel, anim.currentTime);
}
}
glTF.morphing = true;
}
}
}
void updateAnimationBlending(GLTFContext& glTF, AnimationState& anim1, AnimationState& anim2, float weight, float dt)
{
if (anim1.active && anim2.active) {
const Animation& activeAnim1 = glTF.animations[anim1.animId];
anim1.currentTime += activeAnim1.ticksPerSecond * dt;
if (anim1.playOnce && anim1.currentTime > activeAnim1.duration) {
anim1.currentTime = activeAnim1.duration;
anim1.active = false;
} else {
anim1.currentTime = fmodf(anim1.currentTime, activeAnim1.duration);
}
const Animation& activeAnim2 = glTF.animations[anim2.animId];
anim2.currentTime += activeAnim2.ticksPerSecond * dt;
if (anim2.playOnce && anim2.currentTime > activeAnim2.duration) {
anim2.currentTime = activeAnim2.duration;
anim2.active = false;
} else {
anim2.currentTime = fmodf(anim2.currentTime, activeAnim2.duration);
}
// Update skinning
std::function<void(GLTFNodeRef gltfNode, const mat4& parentTransform)> traverseTree = [&](GLTFNodeRef gltfNode,
const mat4& parentTransform) {
const GLTFBone& bone = glTF.bonesByName[glTF.nodesStorage[gltfNode].name];
const uint32_t boneId = bone.boneId;
if (boneId != ~0u) {
auto channel1 = activeAnim1.channels.find(boneId);
auto channel2 = activeAnim2.channels.find(boneId);
if (channel1 != activeAnim1.channels.end() && channel2 != activeAnim2.channels.end()) {
glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId] =
parentTransform *
animationTransformBlending(channel1->second, anim1.currentTime, channel2->second, anim2.currentTime, weight);
} else if (channel1 != activeAnim1.channels.end()) {
glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId] = parentTransform * animationTransform(channel1->second, anim1.currentTime);
} else if (channel2 != activeAnim2.channels.end()) {
glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId] = parentTransform * animationTransform(channel2->second, anim2.currentTime);
} else {
glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId] = parentTransform * glTF.nodesStorage[gltfNode].transform;
}
glTF.skinning = true;
}
for (uint32_t i = 0; i < glTF.nodesStorage[gltfNode].children.size(); i++) {
const uint32_t child = glTF.nodesStorage[gltfNode].children[i];
traverseTree(child, glTF.matrices[glTF.nodesStorage[gltfNode].modelMtxId]);
}
};
traverseTree(glTF.root, mat4(1.0f));
for (const std::pair<std::string, GLTFBone>& b : glTF.bonesByName) {
if (b.second.boneId != ~0u) {
glTF.matrices[b.second.boneId] = glTF.matrices[b.second.boneId] * b.second.transform;
}
}
} else {
glTF.morphing = false;
glTF.skinning = false;
}
}

76
shared/UtilsAnim.h Normal file
View file

@ -0,0 +1,76 @@
#pragma once
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <string>
#include <unordered_map>
#include <vector>
using glm::quat;
using glm::vec3;
using glm::vec4;
struct AnimationKeyPosition {
vec3 pos;
float time;
};
struct AnimationKeyRotation {
quat rot;
float time;
};
struct AnimationKeyScale {
vec3 scale;
float time;
};
struct AnimationChannel {
std::vector<AnimationKeyPosition> pos;
std::vector<AnimationKeyRotation> rot;
std::vector<AnimationKeyScale> scale;
};
#define MAX_MORPH_WEIGHTS 8
#define MAX_MORPHS 100
struct MorphingChannelKey {
float time = 0.0f;
uint32_t mesh[MAX_MORPH_WEIGHTS] = { 0, 0, 0, 0, 0, 0, 0, 0 };
float weight[MAX_MORPH_WEIGHTS] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
};
struct MorphingChannel {
std::string name;
std::vector<MorphingChannelKey> key;
};
struct Animation {
std::unordered_map<int, AnimationChannel> channels;
std::vector<MorphingChannel> morphChannels;
float duration; // In seconds
float ticksPerSecond;
std::string name;
};
struct MorphState {
uint32_t meshId = ~0u;
uint32_t morphTarget[MAX_MORPH_WEIGHTS] = { 0, 0, 0, 0, 0, 0, 0, 0 };
float weights[MAX_MORPH_WEIGHTS] = { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f };
};
struct AnimationState {
uint32_t animId = ~0u;
float currentTime = 0.0f;
bool playOnce = false;
bool active = false;
};
struct GLTFContext;
struct aiScene;
void initAnimations(GLTFContext& glTF, const aiScene* scene);
void updateAnimation(GLTFContext& glTF, AnimationState& anim, float dt);
void updateAnimationBlending(GLTFContext& glTF, AnimationState& anim1, AnimationState& anim2, float weight, float dt);

281
shared/UtilsCubemap.cpp Normal file
View file

@ -0,0 +1,281 @@
#include "UtilsMath.h"
#include "UtilsCubemap.h"
#include <cstdio>
#include <glm/glm.hpp>
#include <glm/ext.hpp>
#include "stb_image_resize2.h"
using glm::vec2;
using glm::vec3;
using glm::vec4;
using glm::ivec2;
/// From Henry J. Warren's "Hacker's Delight"
float radicalInverse_VdC(uint32_t bits)
{
bits = (bits << 16u) | (bits >> 16u);
bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
return float(bits) * 2.3283064365386963e-10f; // / 0x100000000
}
// The i-th point is then computed by
/// From http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
vec2 hammersley2d(uint32_t i, uint32_t N)
{
return vec2(float(i)/float(N), radicalInverse_VdC(i));
}
void convolveLambertian(const vec3* data, int srcW, int srcH, int dstW, int dstH, vec3* output, int numMonteCarloSamples)
{
// only equirectangular maps are supported
assert(srcW == 2 * srcH);
if (srcW != 2 * srcH) return;
std::vector<vec3> tmp(dstW * dstH);
stbir_resize(
reinterpret_cast<const float*>(data), srcW, srcH, 0, reinterpret_cast<float*>(tmp.data()), dstW, dstH, 0, STBIR_RGB, STBIR_TYPE_FLOAT,
STBIR_EDGE_WRAP, STBIR_FILTER_CUBICBSPLINE);
const vec3* scratch = tmp.data();
srcW = dstW;
srcH = dstH;
for (int y = 0; y != dstH; y++)
{
printf("Line %i...\n", y);
const float theta1 = float(y) / float(dstH) * Math::PI;
for (int x = 0; x != dstW; x++)
{
const float phi1 = float(x) / float(dstW) * Math::TWOPI;
const vec3 V1 = vec3(sin(theta1) * cos(phi1), sin(theta1) * sin(phi1), cos(theta1));
vec3 color = vec3(0.0f);
float weight = 0.0f;
for (int i = 0; i != numMonteCarloSamples; i++)
{
const vec2 h = hammersley2d(i, numMonteCarloSamples);
const int x1 = int(floor(h.x * srcW));
const int y1 = int(floor(h.y * srcH));
const float theta2 = float(y1) / float(srcH) * Math::PI;
const float phi2 = float(x1) / float(srcW) * Math::TWOPI;
const vec3 V2 = vec3(sin(theta2) * cos(phi2), sin(theta2) * sin(phi2), cos(theta2));
const float D = std::max(0.0f, glm::dot(V1, V2));
if (D > 0.01f)
{
color += scratch[y1 * srcW + x1] * D;
weight += D;
}
}
output[y * dstW + x] = color / weight;
}
}
}
void convolveGGX(const vec3* data, int srcW, int srcH, int dstW, int dstH, vec3* output, int numMonteCarloSamples)
{
// only equirectangular maps are supported
assert(srcW == 2 * srcH);
if (srcW != 2 * srcH)
return;
std::vector<vec3> tmp(dstW * dstH);
stbir_resize(
reinterpret_cast<const float*>(data), srcW, srcH, 0, reinterpret_cast<float*>(tmp.data()), dstW, dstH, 0, STBIR_RGB, STBIR_TYPE_FLOAT,
STBIR_EDGE_WRAP, STBIR_FILTER_CUBICBSPLINE);
const vec3* scratch = tmp.data();
srcW = dstW;
srcH = dstH;
for (int y = 0; y != dstH; y++) {
printf("Line %i...\n", y);
const float theta1 = float(y) / float(dstH) * Math::PI;
for (int x = 0; x != dstW; x++) {
const float phi1 = float(x) / float(dstW) * Math::TWOPI;
const vec3 V1 = vec3(sin(theta1) * cos(phi1), sin(theta1) * sin(phi1), cos(theta1));
vec3 color = vec3(0.0f);
float weight = 0.0f;
for (int i = 0; i != numMonteCarloSamples; i++) {
const vec2 h = hammersley2d(i, numMonteCarloSamples);
const int x1 = int(floor(h.x * srcW));
const int y1 = int(floor(h.y * srcH));
const float theta2 = float(y1) / float(srcH) * Math::PI;
const float phi2 = float(x1) / float(srcW) * Math::TWOPI;
const vec3 V2 = vec3(sin(theta2) * cos(phi2), sin(theta2) * sin(phi2), cos(theta2));
const float D = std::max(0.0f, glm::dot(V1, V2));
if (D > 0.01f) {
color += scratch[y1 * srcW + x1] * D;
weight += D;
}
}
output[y * dstW + x] = color / weight;
}
}
}
vec3 faceCoordsToXYZ(int i, int j, int faceID, int faceSize)
{
const float A = 2.0f * float(i) / faceSize;
const float B = 2.0f * float(j) / faceSize;
if (faceID == 0) return vec3(-1.0f, A - 1.0f, B - 1.0f);
if (faceID == 1) return vec3(A - 1.0f, -1.0f, 1.0f - B);
if (faceID == 2) return vec3(1.0f, A - 1.0f, 1.0f - B);
if (faceID == 3) return vec3(1.0f - A, 1.0f, 1.0f - B);
if (faceID == 4) return vec3(B - 1.0f, A - 1.0f, 1.0f);
if (faceID == 5) return vec3(1.0f - B, A - 1.0f, -1.0f);
return vec3();
}
Bitmap convertEquirectangularMapToVerticalCross(const Bitmap& b)
{
if (b.type_ != eBitmapType_2D) return Bitmap();
const int faceSize = b.w_ / 4;
const int w = faceSize * 3;
const int h = faceSize * 4;
Bitmap result(w, h, b.comp_, b.fmt_);
const ivec2 kFaceOffsets[] =
{
ivec2(faceSize, faceSize * 3),
ivec2(0, faceSize),
ivec2(faceSize, faceSize),
ivec2(faceSize * 2, faceSize),
ivec2(faceSize, 0),
ivec2(faceSize, faceSize * 2)
};
const int clampW = b.w_ - 1;
const int clampH = b.h_ - 1;
for (int face = 0; face != 6; face++)
{
for (int i = 0; i != faceSize; i++)
{
for (int j = 0; j != faceSize; j++)
{
const vec3 P = faceCoordsToXYZ(i, j, face, faceSize);
const float R = hypot(P.x, P.y);
const float theta = atan2(P.y, P.x);
const float phi = atan2(P.z, R);
// float point source coordinates
const float Uf = float(2.0f * faceSize * (theta + M_PI) / M_PI);
const float Vf = float(2.0f * faceSize * (M_PI / 2.0f - phi) / M_PI);
// 4-samples for bilinear interpolation
const int U1 = clamp(int(floor(Uf)), 0, clampW);
const int V1 = clamp(int(floor(Vf)), 0, clampH);
const int U2 = clamp(U1 + 1, 0, clampW);
const int V2 = clamp(V1 + 1, 0, clampH);
// fractional part
const float s = Uf - U1;
const float t = Vf - V1;
// fetch 4-samples
const vec4 A = b.getPixel(U1, V1);
const vec4 B = b.getPixel(U2, V1);
const vec4 C = b.getPixel(U1, V2);
const vec4 D = b.getPixel(U2, V2);
// bilinear interpolation
const vec4 color = A * (1 - s) * (1 - t) + B * (s) * (1 - t) + C * (1 - s) * t + D * (s) * (t);
result.setPixel(i + kFaceOffsets[face].x, j + kFaceOffsets[face].y, color);
}
};
}
return result;
}
Bitmap convertVerticalCrossToCubeMapFaces(const Bitmap& b)
{
const int faceWidth = b.w_ / 3;
const int faceHeight = b.h_ / 4;
Bitmap cubemap(faceWidth, faceHeight, 6, b.comp_, b.fmt_);
cubemap.type_ = eBitmapType_Cube;
const uint8_t* src = b.data_.data();
uint8_t* dst = cubemap.data_.data();
/*
------
| +Y |
----------------
| -X | -Z | +X |
----------------
| -Y |
------
| +Z |
------
*/
const int pixelSize = cubemap.comp_ * Bitmap::getBytesPerComponent(cubemap.fmt_);
for (int face = 0; face != 6; ++face) {
for (int j = 0; j != faceHeight; ++j) {
for (int i = 0; i != faceWidth; ++i) {
int x = 0;
int y = 0;
switch (face) {
// CUBE_MAP_POSITIVE_X
case 0:
x = 2 * faceWidth + i;
y = 1 * faceHeight + j;
break;
// CUBE_MAP_NEGATIVE_X
case 1:
x = i;
y = faceHeight + j;
break;
// CUBE_MAP_POSITIVE_Y
case 2:
x = 1 * faceWidth + i;
y = j;
break;
// CUBE_MAP_NEGATIVE_Y
case 3:
x = 1 * faceWidth + i;
y = 2 * faceHeight + j;
break;
// CUBE_MAP_POSITIVE_Z
case 4:
x = faceWidth + i;
y = faceHeight + j;
break;
// CUBE_MAP_NEGATIVE_Z
case 5:
x = 2 * faceWidth - (i + 1);
y = b.h_ - (j + 1);
break;
}
memcpy(dst, src + (y * b.w_ + x) * pixelSize, pixelSize);
dst += pixelSize;
}
}
}
return cubemap;
}

15
shared/UtilsCubemap.h Normal file
View file

@ -0,0 +1,15 @@
#pragma once
#include <glm/glm.hpp>
#include "shared/Bitmap.h"
Bitmap convertEquirectangularMapToVerticalCross(const Bitmap& b);
Bitmap convertVerticalCrossToCubeMapFaces(const Bitmap& b);
inline Bitmap convertEquirectangularMapToCubeMapFaces(const Bitmap& b) {
return convertVerticalCrossToCubeMapFaces(convertEquirectangularMapToVerticalCross(b));
}
void convolveLambertian(const glm::vec3* data, int srcW, int srcH, int dstW, int dstH, glm::vec3* output, int numMonteCarloSamples);
void convolveGGX(const glm::vec3* data, int srcW, int srcH, int dstW, int dstH, glm::vec3* output, int numMonteCarloSamples);

43
shared/UtilsFPS.h Normal file
View file

@ -0,0 +1,43 @@
#pragma once
#include <assert.h>
#include <stdio.h>
class FramesPerSecondCounter
{
public:
explicit FramesPerSecondCounter(float avgInterval = 0.5f)
: avgInterval_(avgInterval)
{
assert(avgInterval > 0.0f);
}
bool tick(float deltaSeconds, bool frameRendered = true)
{
if (frameRendered)
numFrames_++;
accumulatedTime_ += deltaSeconds;
if (accumulatedTime_ > avgInterval_) {
currentFPS_ = static_cast<float>(numFrames_ / accumulatedTime_);
if (printFPS_)
printf("FPS: %.1f\n", currentFPS_);
numFrames_ = 0;
accumulatedTime_ = 0;
return true;
}
return false;
}
inline float getFPS() const { return currentFPS_; }
bool printFPS_ = true;
public:
float avgInterval_ = 0.5f;
unsigned int numFrames_ = 0;
double accumulatedTime_ = 0;
float currentFPS_ = 0.0f;
};

1257
shared/UtilsGLTF.cpp Normal file

File diff suppressed because it is too large Load diff

528
shared/UtilsGLTF.h Normal file
View file

@ -0,0 +1,528 @@
#pragma once
#include "VulkanApp.h"
#include <assimp/GltfMaterial.h>
#include <assimp/cimport.h>
#include <assimp/postprocess.h>
#include <assimp/scene.h>
#include <assimp/types.h>
#include "LineCanvas.h"
#include "UtilsAnim.h"
#include <lvk/LVK.h>
enum MaterialType : uint32_t {
MaterialType_Invalid = 0,
MaterialType_Unlit = 0x80,
MaterialType_MetallicRoughness = 0x1,
MaterialType_SpecularGlossiness = 0x2,
MaterialType_Sheen = 0x4,
MaterialType_ClearCoat = 0x8,
MaterialType_Specular = 0x10,
MaterialType_Transmission = 0x20,
MaterialType_Volume = 0x40,
};
const uint32_t kMaxMaterials = 128;
const uint32_t kMaxEnvironments = 4;
const uint32_t kMaxLights = 8;
using glm::mat4;
using glm::quat;
using glm::vec2;
using glm::vec3;
using glm::vec4;
inline mat4 aiMatrix4x4ToMat4(const aiMatrix4x4& from)
{
mat4 to;
to[0][0] = (float)from.a1;
to[0][1] = (float)from.b1;
to[0][2] = (float)from.c1;
to[0][3] = (float)from.d1;
to[1][0] = (float)from.a2;
to[1][1] = (float)from.b2;
to[1][2] = (float)from.c2;
to[1][3] = (float)from.d2;
to[2][0] = (float)from.a3;
to[2][1] = (float)from.b3;
to[2][2] = (float)from.c3;
to[2][3] = (float)from.d3;
to[3][0] = (float)from.a4;
to[3][1] = (float)from.b4;
to[3][2] = (float)from.c4;
to[3][3] = (float)from.d4;
return to;
}
inline vec3 aiVector3DToVec3(const aiVector3D& from)
{
return vec3(from.x, from.y, from.z);
}
inline glm::quat aiQuaternionToQuat(const aiQuaternion& from)
{
return glm::quat(from.w, from.x, from.y, from.z);
}
enum LightType : uint32_t {
LightType_Directional = 0,
LightType_Point = 1,
LightType_Spot = 2,
};
struct EnvironmentMapDataGPU {
uint32_t envMapTexture = 0;
uint32_t envMapTextureSampler = 0;
uint32_t envMapTextureIrradiance = 0;
uint32_t envMapTextureIrradianceSampler = 0;
uint32_t lutBRDFTexture = 0;
uint32_t lutBRDFTextureSampler = 0;
uint32_t envMapTextureCharlie = 0;
uint32_t envMapTextureCharlieSampler = 0;
};
struct LightDataGPU {
vec3 direction = vec3(0, 0, 1);
float range = 10000.0;
vec3 color = vec3(1, 1, 1);
float intensity = 1.0;
vec3 position = vec3(0, 0, -5);
float innerConeCos = 0.0;
float outerConeCos = 0.78;
LightType type = LightType_Directional;
int nodeId = ~0;
int padding = 0;
};
struct GLTFGlobalSamplers {
explicit GLTFGlobalSamplers(const std::unique_ptr<lvk::IContext>& ctx)
{
clamp = ctx->createSampler({
.minFilter = lvk::SamplerFilter::SamplerFilter_Linear,
.magFilter = lvk::SamplerFilter::SamplerFilter_Linear,
.mipMap = lvk::SamplerMip::SamplerMip_Linear,
.wrapU = lvk::SamplerWrap::SamplerWrap_Clamp,
.wrapV = lvk::SamplerWrap::SamplerWrap_Clamp,
.wrapW = lvk::SamplerWrap::SamplerWrap_Clamp,
.debugName = "Clamp Sampler",
});
wrap = ctx->createSampler({
.minFilter = lvk::SamplerFilter::SamplerFilter_Linear,
.magFilter = lvk::SamplerFilter::SamplerFilter_Linear,
.mipMap = lvk::SamplerMip::SamplerMip_Linear,
.wrapU = lvk::SamplerWrap::SamplerWrap_Repeat,
.wrapV = lvk::SamplerWrap::SamplerWrap_Repeat,
.wrapW = lvk::SamplerWrap::SamplerWrap_Repeat,
.debugName = "Wrap Sampler",
});
mirror = ctx->createSampler({
.minFilter = lvk::SamplerFilter::SamplerFilter_Linear,
.magFilter = lvk::SamplerFilter::SamplerFilter_Linear,
.mipMap = lvk::SamplerMip::SamplerMip_Linear,
.wrapU = lvk::SamplerWrap::SamplerWrap_MirrorRepeat,
.wrapV = lvk::SamplerWrap::SamplerWrap_MirrorRepeat,
.debugName = "Mirror Sampler",
});
}
lvk::Holder<lvk::SamplerHandle> clamp;
lvk::Holder<lvk::SamplerHandle> wrap;
lvk::Holder<lvk::SamplerHandle> mirror;
};
struct EnvironmentsPerFrame {
EnvironmentMapDataGPU environments[kMaxEnvironments];
};
struct Vertex {
vec3 position;
vec3 normal;
vec4 color;
vec2 uv0;
vec2 uv1;
float padding[2];
};
struct MorphTarget {
uint32_t meshId = ~0;
std::vector<uint32_t> offset;
};
static_assert(sizeof(Vertex) == sizeof(uint32_t) * 16);
struct GLTFMaterialTextures {
// Metallic Roughness / SpecluarGlossiness
lvk::Holder<lvk::TextureHandle> baseColorTexture;
lvk::Holder<lvk::TextureHandle> surfacePropertiesTexture;
// Common properties
lvk::Holder<lvk::TextureHandle> normalTexture;
lvk::Holder<lvk::TextureHandle> occlusionTexture;
lvk::Holder<lvk::TextureHandle> emissiveTexture;
// Sheen
lvk::Holder<lvk::TextureHandle> sheenColorTexture;
lvk::Holder<lvk::TextureHandle> sheenRoughnessTexture;
// Clearcoat
lvk::Holder<lvk::TextureHandle> clearCoatTexture;
lvk::Holder<lvk::TextureHandle> clearCoatRoughnessTexture;
lvk::Holder<lvk::TextureHandle> clearCoatNormalTexture;
// Specular
lvk::Holder<lvk::TextureHandle> specularTexture;
lvk::Holder<lvk::TextureHandle> specularColorTexture;
// Transmission
lvk::Holder<lvk::TextureHandle> transmissionTexture;
// Volumen
lvk::Holder<lvk::TextureHandle> thicknessTexture;
// Iridescence
lvk::Holder<lvk::TextureHandle> iridescenceTexture;
lvk::Holder<lvk::TextureHandle> iridescenceThicknessTexture;
// Anisotropy
lvk::Holder<lvk::TextureHandle> anisotropyTexture;
lvk::Holder<lvk::TextureHandle> white;
bool wasLoaded = false;
};
struct EnvironmentMapTextures {
explicit EnvironmentMapTextures(const std::unique_ptr<lvk::IContext>& ctx)
: EnvironmentMapTextures(
ctx, "data/brdfLUT.ktx", "data/piazza_bologni_1k_prefilter.ktx", "data/piazza_bologni_1k_irradiance.ktx",
"data/piazza_bologni_1k_charlie.ktx")
{
}
EnvironmentMapTextures(
const std::unique_ptr<lvk::IContext>& ctx, const char* brdfLUT, const char* prefilter, const char* irradiance,
const char* prefilterCharlie = nullptr)
{
texBRDF_LUT = loadTexture(ctx, brdfLUT, lvk::TextureType_2D);
if (texBRDF_LUT.empty()) {
assert(0);
exit(255);
}
envMapTexture = loadTexture(ctx, prefilter, lvk::TextureType_Cube);
if (envMapTexture.empty()) {
assert(0);
exit(255);
}
envMapTextureIrradiance = loadTexture(ctx, irradiance, lvk::TextureType_Cube);
if (envMapTextureIrradiance.empty()) {
assert(0);
exit(255);
}
if (prefilterCharlie) {
envMapTextureCharlie = loadTexture(ctx, prefilterCharlie, lvk::TextureType_Cube);
if (envMapTextureCharlie.empty()) {
assert(0);
exit(255);
}
}
}
lvk::Holder<lvk::TextureHandle> texBRDF_LUT;
lvk::Holder<lvk::TextureHandle> envMapTexture;
lvk::Holder<lvk::TextureHandle> envMapTextureCharlie;
lvk::Holder<lvk::TextureHandle> envMapTextureIrradiance;
};
bool assignUVandSampler(
const GLTFGlobalSamplers& samplers, const aiMaterial* mtlDescriptor, aiTextureType textureType, uint32_t& uvIndex,
uint32_t& textureSampler, int index = 0);
struct GLTFMaterialDataGPU {
vec4 baseColorFactor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
vec4 metallicRoughnessNormalOcclusion =
vec4(1.0f, 1.0f, 1.0f, 1.0f); // Packed metallicFactor, roughnessFactor, normalScale, occlusionStrength
vec4 specularGlossiness = vec4(1.0f, 1.0f, 1.0f, 1.0f);
vec4 sheenFactors = vec4(1.0f, 1.0f, 1.0f, 1.0f); // Sheen
vec4 clearcoatTransmissionThickness = vec4(1.0f, 1.0f, 1.0f, 1.0f); // Clearcoat
vec4 specularFactors = vec4(1.0f, 1.0f, 1.0f, 1.0f);
vec4 attenuation = vec4(1.0f, 1.0f, 1.0f, 1.0f);
vec4 emissiveFactorAlphaCutoff = vec4(0.0f, 0.0f, 0.0f, 0.5f);
uint32_t occlusionTexture = 0;
uint32_t occlusionTextureSampler = 0;
uint32_t occlusionTextureUV = 0;
uint32_t emissiveTexture = 0;
uint32_t emissiveTextureSampler = 0;
uint32_t emissiveTextureUV = 0;
uint32_t baseColorTexture = 0;
uint32_t baseColorTextureSampler = 0;
uint32_t baseColorTextureUV = 0;
uint32_t surfacePropertiesTexture = 0;
uint32_t surfacePropertiesTextureSampler = 0;
uint32_t surfacePropertiesTextureUV = 0;
uint32_t normalTexture = 0;
uint32_t normalTextureSampler = 0;
uint32_t normalTextureUV = 0;
uint32_t sheenColorTexture = 0;
uint32_t sheenColorTextureSampler = 0;
uint32_t sheenColorTextureUV = 0;
uint32_t sheenRoughnessTexture = 0;
uint32_t sheenRoughnessTextureSampler = 0;
uint32_t sheenRoughnessTextureUV = 0;
uint32_t clearCoatTexture = 0;
uint32_t clearCoatTextureSampler = 0;
uint32_t clearCoatTextureUV = 0;
uint32_t clearCoatRoughnessTexture = 0;
uint32_t clearCoatRoughnessTextureSampler = 0;
uint32_t clearCoatRoughnessTextureUV = 0;
uint32_t clearCoatNormalTexture = 0;
uint32_t clearCoatNormalTextureSampler = 0;
uint32_t clearCoatNormalTextureUV = 0;
uint32_t specularTexture = 0;
uint32_t specularTextureSampler = 0;
uint32_t specularTextureUV = 0;
uint32_t specularColorTexture = 0;
uint32_t specularColorTextureSampler = 0;
uint32_t specularColorTextureUV = 0;
uint32_t transmissionTexture = 0;
uint32_t transmissionTextureSampler = 0;
uint32_t transmissionTextureUV = 0;
uint32_t thicknessTexture = 0;
uint32_t thicknessTextureSampler = 0;
uint32_t thicknessTextureUV = 0;
uint32_t iridescenceTexture = 0;
uint32_t iridescenceTextureSampler = 0;
uint32_t iridescenceTextureUV = 0;
uint32_t iridescenceThicknessTexture = 0;
uint32_t iridescenceThicknessTextureSampler = 0;
uint32_t iridescenceThicknessTextureUV = 0;
uint32_t anisotropyTexture = 0;
uint32_t anisotropyTextureSampler = 0;
uint32_t anisotropyTextureUV = 0;
uint32_t alphaMode = 0;
uint32_t materialTypeFlags = 0;
float ior = 1.5f;
uint32_t padding[2] = { 0, 0 };
enum AlphaMode : uint32_t {
AlphaMode_Opaque = 0,
AlphaMode_Mask = 1,
AlphaMode_Blend = 2,
};
};
static_assert(sizeof(GLTFMaterialDataGPU) % 16 == 0);
struct GLTFDataHolder {
std::vector<GLTFMaterialTextures> textures;
};
struct MaterialsPerFrame {
GLTFMaterialDataGPU materials[kMaxMaterials];
};
using GLTFNodeRef = uint32_t;
using GLTFMeshRef = uint32_t;
enum SortingType : uint32_t {
SortingType_Opaque = 0,
SortingType_Transmission = 1,
SortingType_Transparent = 2,
};
struct GLTFMesh {
lvk::Topology primitive;
uint32_t vertexOffset;
uint32_t vertexCount;
uint32_t indexOffset;
uint32_t indexCount;
uint32_t matIdx;
SortingType sortingType;
};
struct GLTFNode {
std::string name;
uint32_t modelMtxId;
mat4 transform = mat4(1);
std::vector<GLTFNodeRef> children;
std::vector<GLTFMeshRef> meshes;
};
struct GLTFFrameData {
mat4 model;
mat4 view;
mat4 proj;
vec4 cameraPos;
};
struct GLTFCamera {
std::string name;
uint32_t nodeIdx = ~0;
vec3 pos;
vec3 up;
vec3 lookAt;
float hFOV;
float near;
float far;
float aspect = 1.0f;
float orthoWidth;
mat4 getProjection(float windowAspect = 1.0f) const
{
return orthoWidth != 0.0f
? glm::ortho(-windowAspect / orthoWidth, windowAspect / orthoWidth, -1.0f / orthoWidth, 1.0f / orthoWidth, far, near)
: glm::perspective(hFOV, windowAspect == 1.0f ? aspect : windowAspect, near, far);
}
};
struct GLTFTransforms {
uint32_t modelMtxId;
uint32_t matId;
GLTFNodeRef nodeRef; // for CPU only
GLTFMeshRef meshRef; // for CPU only
uint32_t sortingType;
};
// Skeleton, animation, morphing
#define MAX_BONES_PER_VERTEX 8
struct VertexBoneData {
vec4 position;
vec4 normal;
uint32_t boneId[MAX_BONES_PER_VERTEX] = { ~0u, ~0u, ~0u, ~0u, ~0u, ~0u, ~0u, ~0u };
float weight[MAX_BONES_PER_VERTEX] = {};
uint32_t meshId = ~0u;
};
static_assert(sizeof(VertexBoneData) == sizeof(uint32_t) * 25);
struct GLTFBone {
uint32_t boneId = ~0u;
mat4 transform = mat4(1);
};
GLTFMaterialDataGPU setupglTFMaterialData(
const std::unique_ptr<lvk::IContext>& ctx, const GLTFGlobalSamplers& samplers, aiMaterial* const& mtlDescriptor,
const char* assetFolder, GLTFDataHolder& glTFDataholder, bool& useVolumetric);
struct GLTFContext {
explicit GLTFContext(VulkanApp& app_)
: app(app_)
, samplers(app_.ctx_)
, envMapTextures(app_.ctx_)
{
}
GLTFDataHolder glTFDataholder;
MaterialsPerFrame matPerFrame;
GLTFGlobalSamplers samplers;
EnvironmentMapTextures envMapTextures;
GLTFFrameData frameData;
std::vector<GLTFTransforms> transforms;
std::vector<mat4> matrices;
std::vector<GLTFNode> nodesStorage;
std::vector<GLTFMesh> meshesStorage;
std::unordered_map<std::string, GLTFBone> bonesByName;
std::vector<MorphTarget> morphTargets;
std::unordered_map<std::string, uint32_t> meshesRemap;
std::vector<Animation> animations;
std::vector<uint32_t> opaqueNodes;
std::vector<uint32_t> transmissionNodes;
std::vector<uint32_t> transparentNodes;
lvk::Holder<lvk::BufferHandle> envBuffer;
lvk::Holder<lvk::BufferHandle> lightsBuffer;
lvk::Holder<lvk::BufferHandle> perFrameBuffer;
lvk::Holder<lvk::BufferHandle> transformBuffer;
lvk::Holder<lvk::BufferHandle> matricesBuffer;
lvk::Holder<lvk::BufferHandle> morphStatesBuffer;
lvk::Holder<lvk::RenderPipelineHandle> pipelineSolid;
lvk::Holder<lvk::RenderPipelineHandle> pipelineTransparent;
lvk::Holder<lvk::ComputePipelineHandle> pipelineComputeAnimations;
lvk::Holder<lvk::ShaderModuleHandle> vert;
lvk::Holder<lvk::ShaderModuleHandle> frag;
lvk::Holder<lvk::ShaderModuleHandle> animation;
lvk::Holder<lvk::BufferHandle> vertexBuffer;
lvk::Holder<lvk::BufferHandle> vertexSkinningBuffer;
lvk::Holder<lvk::BufferHandle> vertexMorphingBuffer;
lvk::Holder<lvk::BufferHandle> indexBuffer;
lvk::Holder<lvk::BufferHandle> matBuffer;
lvk::Holder<lvk::TextureHandle> offscreenTex[3] = {};
uint32_t currentOffscreenTex = 0;
uint32_t maxVertices = 0;
std::vector<MorphState> morphStates;
std::vector<LightDataGPU> lights;
std::vector<GLTFCamera> cameras;
GLTFIntrospective inspector;
GLTFNodeRef root;
VulkanApp& app;
LineCanvas3D canvas3d;
bool hasBones = false;
bool isVolumetricMaterial = false;
bool animated = false;
bool skinning = false;
bool morphing = false;
bool doublesided = false;
bool enableMorphing = true;
bool isScreenCopyRequired() const { return isVolumetricMaterial; }
};
void loadGLTF(GLTFContext& context, const char* gltfName, const char* glTFDataPath);
void renderGLTF(GLTFContext& context, const mat4& model, const mat4& view, const mat4& proj, bool rebuildRenderList = false);
void animateGLTF(GLTFContext& gltf, AnimationState& anim, float dt);
void animateBlendingGLTF(GLTFContext& gltf, AnimationState& anim1, AnimationState& anim2, float weight, float dt);
MaterialType detectMaterialType(const aiMaterial* mtl);
void printPrefix(int ofs);
void printMat4(const aiMatrix4x4& m);
std::vector<std::string> camerasGLTF(GLTFContext& context);
void updateCamera(GLTFContext& gltf, const mat4& model, mat4& view, mat4& proj, float aspectRatio);
std::vector<std::string> animationsGLTF(GLTFContext& gltf);

199
shared/UtilsMath.h Normal file
View file

@ -0,0 +1,199 @@
#pragma once
#define _USE_MATH_DEFINES
#include <cmath>
#include <glm/ext.hpp>
#include <glm/glm.hpp>
#include <vector>
using glm::mat4;
using glm::vec2;
using glm::vec3;
using glm::vec4;
namespace Math
{
static constexpr float PI = 3.14159265359f;
static constexpr float TWOPI = 6.28318530718f;
} // namespace Math
inline vec2 clampLength(const vec2& v, float maxLength)
{
const float l = length(v);
return (l > maxLength) ? normalize(v) * maxLength : v;
}
struct BoundingBox {
vec3 min_;
vec3 max_;
BoundingBox() = default;
BoundingBox(const vec3& min, const vec3& max)
: min_(glm::min(min, max))
, max_(glm::max(min, max))
{
}
BoundingBox(const vec3* points, size_t numPoints)
{
vec3 vmin(std::numeric_limits<float>::max());
vec3 vmax(std::numeric_limits<float>::lowest());
for (size_t i = 0; i != numPoints; i++) {
vmin = glm::min(vmin, points[i]);
vmax = glm::max(vmax, points[i]);
}
min_ = vmin;
max_ = vmax;
}
vec3 getSize() const { return vec3(max_[0] - min_[0], max_[1] - min_[1], max_[2] - min_[2]); }
vec3 getCenter() const { return 0.5f * vec3(max_[0] + min_[0], max_[1] + min_[1], max_[2] + min_[2]); }
void transform(const glm::mat4& t)
{
vec3 corners[] = {
vec3(min_.x, min_.y, min_.z), vec3(min_.x, max_.y, min_.z), vec3(min_.x, min_.y, max_.z), vec3(min_.x, max_.y, max_.z),
vec3(max_.x, min_.y, min_.z), vec3(max_.x, max_.y, min_.z), vec3(max_.x, min_.y, max_.z), vec3(max_.x, max_.y, max_.z),
};
for (auto& v : corners)
v = vec3(t * vec4(v, 1.0f));
*this = BoundingBox(corners, 8);
}
BoundingBox getTransformed(const glm::mat4& t) const
{
BoundingBox b = *this;
b.transform(t);
return b;
}
void combinePoint(const vec3& p)
{
min_ = glm::min(min_, p);
max_ = glm::max(max_, p);
}
};
template <typename T> T clamp(T v, T a, T b)
{
if (v < a)
return a;
if (v > b)
return b;
return v;
}
inline float random01()
{
return (float)rand() / (float)RAND_MAX;
}
inline float randomFloat(float min, float max)
{
return min + (max - min) * random01();
}
inline vec3 randomVec(const vec3& min, const vec3& max)
{
return vec3(randomFloat(min.x, max.x), randomFloat(min.y, max.y), randomFloat(min.z, max.z));
}
inline vec3 randVec()
{
return randomVec(vec3(-5, -5, -5), vec3(5, 5, 5));
}
inline void getFrustumPlanes(mat4 viewProj, vec4* planes)
{
viewProj = glm::transpose(viewProj);
planes[0] = vec4(viewProj[3] + viewProj[0]); // left
planes[1] = vec4(viewProj[3] - viewProj[0]); // right
planes[2] = vec4(viewProj[3] + viewProj[1]); // bottom
planes[3] = vec4(viewProj[3] - viewProj[1]); // top
planes[4] = vec4(viewProj[3] + viewProj[2]); // near
planes[5] = vec4(viewProj[3] - viewProj[2]); // far
}
inline void getFrustumCorners(mat4 viewProj, vec4* points)
{
const vec4 corners[] = { vec4(-1, -1, -1, 1), vec4(1, -1, -1, 1), vec4(1, 1, -1, 1), vec4(-1, 1, -1, 1),
vec4(-1, -1, 1, 1), vec4(1, -1, 1, 1), vec4(1, 1, 1, 1), vec4(-1, 1, 1, 1) };
const glm::mat4 invViewProj = glm::inverse(viewProj);
for (int i = 0; i != 8; i++) {
const vec4 q = invViewProj * corners[i];
points[i] = q / q.w;
}
}
inline bool isBoxInFrustum(glm::vec4* frustumPlanes, glm::vec4* frustumCorners, const BoundingBox& box)
{
using glm::dot;
for (int i = 0; i < 6; i++) {
int r = 0;
r += (dot(frustumPlanes[i], vec4(box.min_.x, box.min_.y, box.min_.z, 1.0f)) < 0.0) ? 1 : 0;
r += (dot(frustumPlanes[i], vec4(box.max_.x, box.min_.y, box.min_.z, 1.0f)) < 0.0) ? 1 : 0;
r += (dot(frustumPlanes[i], vec4(box.min_.x, box.max_.y, box.min_.z, 1.0f)) < 0.0) ? 1 : 0;
r += (dot(frustumPlanes[i], vec4(box.max_.x, box.max_.y, box.min_.z, 1.0f)) < 0.0) ? 1 : 0;
r += (dot(frustumPlanes[i], vec4(box.min_.x, box.min_.y, box.max_.z, 1.0f)) < 0.0) ? 1 : 0;
r += (dot(frustumPlanes[i], vec4(box.max_.x, box.min_.y, box.max_.z, 1.0f)) < 0.0) ? 1 : 0;
r += (dot(frustumPlanes[i], vec4(box.min_.x, box.max_.y, box.max_.z, 1.0f)) < 0.0) ? 1 : 0;
r += (dot(frustumPlanes[i], vec4(box.max_.x, box.max_.y, box.max_.z, 1.0f)) < 0.0) ? 1 : 0;
if (r == 8)
return false;
}
// check frustum outside/inside box
int r = 0;
r = 0;
for (int i = 0; i < 8; i++)
r += ((frustumCorners[i].x > box.max_.x) ? 1 : 0);
if (r == 8)
return false;
r = 0;
for (int i = 0; i < 8; i++)
r += ((frustumCorners[i].x < box.min_.x) ? 1 : 0);
if (r == 8)
return false;
r = 0;
for (int i = 0; i < 8; i++)
r += ((frustumCorners[i].y > box.max_.y) ? 1 : 0);
if (r == 8)
return false;
r = 0;
for (int i = 0; i < 8; i++)
r += ((frustumCorners[i].y < box.min_.y) ? 1 : 0);
if (r == 8)
return false;
r = 0;
for (int i = 0; i < 8; i++)
r += ((frustumCorners[i].z > box.max_.z) ? 1 : 0);
if (r == 8)
return false;
r = 0;
for (int i = 0; i < 8; i++)
r += ((frustumCorners[i].z < box.min_.z) ? 1 : 0);
if (r == 8)
return false;
return true;
}
inline BoundingBox combineBoxes(const std::vector<BoundingBox>& boxes)
{
std::vector<vec3> allPoints;
allPoints.reserve(boxes.size() * 8);
for (const auto& b : boxes) {
allPoints.emplace_back(b.min_.x, b.min_.y, b.min_.z);
allPoints.emplace_back(b.min_.x, b.min_.y, b.max_.z);
allPoints.emplace_back(b.min_.x, b.max_.y, b.min_.z);
allPoints.emplace_back(b.min_.x, b.max_.y, b.max_.z);
allPoints.emplace_back(b.max_.x, b.min_.y, b.min_.z);
allPoints.emplace_back(b.max_.x, b.min_.y, b.max_.z);
allPoints.emplace_back(b.max_.x, b.max_.y, b.min_.z);
allPoints.emplace_back(b.max_.x, b.max_.y, b.max_.z);
}
return BoundingBox(allPoints.data(), allPoints.size());
}

412
shared/VulkanApp.cpp Normal file
View file

@ -0,0 +1,412 @@
#include "VulkanApp.h"
#include "UtilsGLTF.h"
#include <unordered_map>
extern std::unordered_map<uint32_t, std::string> debugGLSLSourceCode;
static void shaderModuleCallback(lvk::IContext*, lvk::ShaderModuleHandle handle, int line, int col, const char* debugName)
{
const auto it = debugGLSLSourceCode.find(handle.index());
if (it != debugGLSLSourceCode.end()) {
lvk::logShaderSource(it->second.c_str());
}
}
VulkanApp::VulkanApp(const VulkanAppConfig& cfg)
: cfg_(cfg)
{
minilog::initialize(nullptr, { .threadNames = false });
int width = -95;
int height = -90;
window_ = lvk::initWindow("Simple example", width, height);
ctx_ = lvk::createVulkanContextWithSwapchain(
window_, width, height,
{
.enableValidation = true,
.shaderModuleErrorCallback = &shaderModuleCallback,
});
depthTexture_ = ctx_->createTexture({
.type = lvk::TextureType_2D,
.format = lvk::Format_Z_F32,
.dimensions = {(uint32_t)width, (uint32_t)height},
.usage = lvk::TextureUsageBits_Attachment,
.debugName = "Depth buffer",
});
imgui_ = std::make_unique<lvk::ImGuiRenderer>(*ctx_, "data/OpenSans-Light.ttf", 30.0f);
implotCtx_ = ImPlot::CreateContext();
glfwSetWindowUserPointer(window_, this);
glfwSetMouseButtonCallback(window_, [](GLFWwindow* window, int button, int action, int mods) {
VulkanApp* app = (VulkanApp*)glfwGetWindowUserPointer(window);
if (button == GLFW_MOUSE_BUTTON_LEFT) {
app->mouseState_.pressedLeft = action == GLFW_PRESS;
}
double xpos, ypos;
glfwGetCursorPos(window, &xpos, &ypos);
const ImGuiMouseButton_ imguiButton = (button == GLFW_MOUSE_BUTTON_LEFT)
? ImGuiMouseButton_Left
: (button == GLFW_MOUSE_BUTTON_RIGHT ? ImGuiMouseButton_Right : ImGuiMouseButton_Middle);
ImGuiIO& io = ImGui::GetIO();
io.MousePos = ImVec2((float)xpos, (float)ypos);
io.MouseDown[imguiButton] = action == GLFW_PRESS;
for (auto& cb : app->callbacksMouseButton) {
cb(window, button, action, mods);
}
});
glfwSetScrollCallback(window_, [](GLFWwindow* window, double dx, double dy) {
ImGuiIO& io = ImGui::GetIO();
io.MouseWheelH = (float)dx;
io.MouseWheel = (float)dy;
});
glfwSetCursorPosCallback(window_, [](GLFWwindow* window, double x, double y) {
VulkanApp* app = (VulkanApp*)glfwGetWindowUserPointer(window);
int width, height;
glfwGetFramebufferSize(window, &width, &height);
ImGui::GetIO().MousePos = ImVec2(x, y);
app->mouseState_.pos.x = static_cast<float>(x / width);
app->mouseState_.pos.y = 1.0f - static_cast<float>(y / height);
});
glfwSetKeyCallback(window_, [](GLFWwindow* window, int key, int scancode, int action, int mods) {
VulkanApp* app = (VulkanApp*)glfwGetWindowUserPointer(window);
const bool pressed = action != GLFW_RELEASE;
if (key == GLFW_KEY_ESCAPE && pressed)
glfwSetWindowShouldClose(window, GLFW_TRUE);
if (key == GLFW_KEY_W)
app->positioner_.movement_.forward_ = pressed;
if (key == GLFW_KEY_S)
app->positioner_.movement_.backward_ = pressed;
if (key == GLFW_KEY_A)
app->positioner_.movement_.left_ = pressed;
if (key == GLFW_KEY_D)
app->positioner_.movement_.right_ = pressed;
if (key == GLFW_KEY_1)
app->positioner_.movement_.up_ = pressed;
if (key == GLFW_KEY_2)
app->positioner_.movement_.down_ = pressed;
app->positioner_.movement_.fastSpeed_ = (mods & GLFW_MOD_SHIFT) != 0;
if (key == GLFW_KEY_SPACE) {
app->positioner_.lookAt(app->cfg_.initialCameraPos, app->cfg_.initialCameraTarget, vec3(0.0f, 1.0f, 0.0f));
app->positioner_.setSpeed(vec3(0));
}
for (auto& cb : app->callbacksKey) {
cb(window, key, scancode, action, mods);
}
});
}
VulkanApp::~VulkanApp()
{
ImPlot::DestroyContext(implotCtx_);
gridPipeline = nullptr;
gridVert = nullptr;
gridFrag = nullptr;
imgui_ = nullptr;
depthTexture_ = nullptr;
ctx_ = nullptr;
glfwDestroyWindow(window_);
glfwTerminate();
}
lvk::Format VulkanApp::getDepthFormat() const
{
return ctx_->getFormat(depthTexture_);
}
void VulkanApp::run(DrawFrameFunc drawFrame)
{
double timeStamp = glfwGetTime();
float deltaSeconds = 0.0f;
while (!glfwWindowShouldClose(window_)) {
fpsCounter_.tick(deltaSeconds);
const double newTimeStamp = glfwGetTime();
deltaSeconds = static_cast<float>(newTimeStamp - timeStamp);
timeStamp = newTimeStamp;
glfwPollEvents();
int width, height;
#if defined(__APPLE__)
// a hacky workaround for retina displays
glfwGetWindowSize(window_, &width, &height);
#else
glfwGetFramebufferSize(window_, &width, &height);
#endif
if (!width || !height)
continue;
const float ratio = width / (float)height;
positioner_.update(deltaSeconds, mouseState_.pos, ImGui::GetIO().WantCaptureMouse ? false : mouseState_.pressedLeft);
drawFrame((uint32_t)width, (uint32_t)height, ratio, deltaSeconds);
}
}
void VulkanApp::drawMemo()
{
ImGui::SetNextWindowPos(ImVec2(10, 10));
ImGui::Begin(
"Keyboard hints:", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoInputs | ImGuiWindowFlags_NoCollapse);
ImGui::Text("W/S/A/D - camera movement");
ImGui::Text("1/2 - camera up/down");
ImGui::Text("Shift - fast movement");
ImGui::Text("Space - reset view");
ImGui::End();
}
void VulkanApp::drawGTFInspector_Animations(GLTFIntrospective& intro)
{
if (!intro.showAnimations)
return;
if (ImGui::Begin(
"Animations", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoCollapse)) {
for (uint32_t a = 0; a < intro.animations.size(); ++a) {
auto it = std::find(intro.activeAnim.begin(), intro.activeAnim.end(), a);
bool oState = it != intro.activeAnim.end(); // && selected < anim->size();
bool state = oState;
ImGui::Checkbox(intro.animations[a].c_str(), &state);
if (state) {
if (!oState) {
uint32_t freeSlot = intro.activeAnim.size() - 1;
if (auto nf = std::find(intro.activeAnim.begin(), intro.activeAnim.end(), ~0u); nf != intro.activeAnim.end()) {
freeSlot = std::distance(intro.activeAnim.begin(), nf);
}
intro.activeAnim[freeSlot] = a;
}
} else {
if (it != intro.activeAnim.end()) {
*it = ~0;
}
}
}
}
if (intro.showAnimationBlend) {
ImGui::SliderFloat("Blend", &intro.blend, 0, 1.0f);
}
ImGui::End();
}
void VulkanApp::drawGTFInspector_Materials(GLTFIntrospective& intro) {
if (!intro.showMaterials || intro.materials.empty())
return;
if (ImGui::Begin(
"Materials", nullptr,
ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoFocusOnAppearing |
ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoCollapse)) {
for (uint32_t m = 0; m < intro.materials.size(); ++m) {
GLTFMaterialIntro& mat = intro.materials[m];
const uint32_t& currentMask = intro.materials[m].currentMaterialMask;
auto setMaterialMask = [&m = intro.materials[m]](uint32_t flag, bool active) {
m.modified = true;
if (active) {
m.currentMaterialMask |= flag;
} else {
m.currentMaterialMask &= ~flag;
}
};
const bool isUnlit = (currentMask & MaterialType_Unlit) == MaterialType_Unlit;
bool state = false;
ImGui::Text("%s", mat.name.c_str());
ImGui::PushID(m);
state = isUnlit;
if (ImGui::RadioButton("Unlit", state)) {
mat.currentMaterialMask = 0;
setMaterialMask(MaterialType_Unlit, true);
}
state = (currentMask & MaterialType_MetallicRoughness) == MaterialType_MetallicRoughness;
if ((mat.materialMask & MaterialType_MetallicRoughness) == MaterialType_MetallicRoughness) {
if (ImGui::RadioButton("MetallicRoughness", state)) {
setMaterialMask(MaterialType_Unlit, false);
setMaterialMask(MaterialType_SpecularGlossiness, false);
setMaterialMask(MaterialType_MetallicRoughness, true);
}
}
state = (currentMask & MaterialType_SpecularGlossiness) == MaterialType_SpecularGlossiness;
if ((mat.materialMask & MaterialType_SpecularGlossiness) == MaterialType_SpecularGlossiness) {
if (ImGui::RadioButton("SpecularGlossiness", state)) {
setMaterialMask(MaterialType_Unlit, false);
setMaterialMask(MaterialType_SpecularGlossiness, true);
setMaterialMask(MaterialType_MetallicRoughness, false);
}
}
state = (currentMask & MaterialType_Sheen) == MaterialType_Sheen;
if ((mat.materialMask & MaterialType_Sheen) == MaterialType_Sheen) {
ImGui::BeginDisabled(isUnlit);
if (ImGui::Checkbox("Sheen", &state)) {
setMaterialMask(MaterialType_Sheen, state);
}
ImGui::EndDisabled();
}
state = (mat.currentMaterialMask & MaterialType_ClearCoat) == MaterialType_ClearCoat;
if ((mat.materialMask & MaterialType_ClearCoat) == MaterialType_ClearCoat) {
ImGui::BeginDisabled(isUnlit);
if (ImGui::Checkbox("ClearCoat", &state)) {
setMaterialMask(MaterialType_ClearCoat, state);
}
ImGui::EndDisabled();
}
state = (mat.currentMaterialMask & MaterialType_Specular) == MaterialType_Specular;
if ((mat.materialMask & MaterialType_Specular) == MaterialType_Specular) {
ImGui::BeginDisabled(isUnlit);
if (ImGui::Checkbox("Specular", &state)) {
setMaterialMask(MaterialType_Specular, state);
}
ImGui::EndDisabled();
}
state = (mat.currentMaterialMask & MaterialType_Transmission) == MaterialType_Transmission;
if ((mat.materialMask & MaterialType_Transmission) == MaterialType_Transmission) {
ImGui::BeginDisabled(isUnlit);
if (ImGui::Checkbox("Transmission", &state)) {
if (!state) {
setMaterialMask(MaterialType_Volume, false);
}
setMaterialMask(MaterialType_Transmission, state);
}
ImGui::EndDisabled();
}
state = (mat.currentMaterialMask & MaterialType_Volume) == MaterialType_Volume;
if ((mat.materialMask & MaterialType_Volume) == MaterialType_Volume) {
ImGui::BeginDisabled(isUnlit);
if (ImGui::Checkbox("Volume", &state)) {
setMaterialMask(MaterialType_Volume, state);
if (state) {
setMaterialMask(MaterialType_Transmission, true);
}
}
ImGui::EndDisabled();
}
ImGui::PopID();
}
}
ImGui::End();
}
void VulkanApp::drawGTFInspector_Cameras(GLTFIntrospective& intro)
{
if (!intro.showCameras)
return;
ImGui::Begin("Cameras:", nullptr, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoCollapse);
std::string current_item = intro.activeCamera != ~0u ? intro.cameras[intro.activeCamera] : "";
if (ImGui::BeginCombo("##combo", current_item.c_str())) {
for (uint32_t n = 0; n < intro.cameras.size(); n++) {
bool is_selected = (current_item == intro.cameras[n]);
if (ImGui::Selectable(intro.cameras[n].c_str(), is_selected)) {
intro.activeCamera = n;
current_item = intro.cameras[n];
}
if (is_selected)
ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
ImGui::End();
}
void VulkanApp::drawGTFInspector(GLTFIntrospective& intro)
{
if (!cfg_.showGLTFInspector) {
return;
}
ImGui::SetNextWindowPos(ImVec2(10, 300));
drawGTFInspector_Animations(intro);
drawGTFInspector_Materials(intro);
drawGTFInspector_Cameras(intro);
}
void VulkanApp::drawFPS()
{
if (const ImGuiViewport* v = ImGui::GetMainViewport()) {
ImGui::SetNextWindowPos({ v->WorkPos.x + v->WorkSize.x - 15.0f, v->WorkPos.y + 15.0f }, ImGuiCond_Always, { 1.0f, 0.0f });
}
ImGui::SetNextWindowBgAlpha(0.30f);
ImGui::SetNextWindowSize(ImVec2(ImGui::CalcTextSize("FPS : _______").x, 0));
if (ImGui::Begin(
"##FPS", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoMove)) {
ImGui::Text("FPS : %i", (int)fpsCounter_.getFPS());
ImGui::Text("Ms : %.1f", fpsCounter_.getFPS() > 0 ? 1000.0 / fpsCounter_.getFPS() : 0);
}
ImGui::End();
}
void VulkanApp::drawGrid(lvk::ICommandBuffer& buf, const mat4& proj, const vec3& origin, uint32_t numSamples, lvk::Format colorFormat)
{
drawGrid(buf, proj * camera_.getViewMatrix(), origin, camera_.getPosition(), numSamples, colorFormat);
}
void VulkanApp::drawGrid(
lvk::ICommandBuffer& buf, const mat4& mvp, const vec3& origin, const vec3& camPos, uint32_t numSamples, lvk::Format colorFormat)
{
if (gridPipeline.empty() || pipelineSamples != numSamples) {
gridVert = loadShaderModule(ctx_, "data/shaders/Grid.vert");
gridFrag = loadShaderModule(ctx_, "data/shaders/Grid.frag");
pipelineSamples = numSamples;
gridPipeline = ctx_->createRenderPipeline({
.smVert = gridVert,
.smFrag = gridFrag,
.color = { {
.format = colorFormat != lvk::Format_Invalid ? colorFormat : ctx_->getSwapchainFormat(),
.blendEnabled = true,
.srcRGBBlendFactor = lvk::BlendFactor_SrcAlpha,
.dstRGBBlendFactor = lvk::BlendFactor_OneMinusSrcAlpha,
} },
.depthFormat = this->getDepthFormat(),
.samplesCount = numSamples,
.debugName = "Pipeline: drawGrid()",
});
}
const struct {
mat4 mvp;
vec4 camPos;
vec4 origin;
} pc = {
.mvp = mvp,
.camPos = vec4(camPos, 1.0f),
.origin = vec4(origin, 1.0f),
};
buf.cmdPushDebugGroupLabel("Grid", 0xff0000ff);
buf.cmdBindRenderPipeline(gridPipeline);
buf.cmdBindDepthState({ .compareOp = lvk::CompareOp_Less, .isDepthWriteEnabled = false });
buf.cmdPushConstants(pc);
buf.cmdDraw(6);
buf.cmdPopDebugGroupLabel();
}

119
shared/VulkanApp.h Normal file
View file

@ -0,0 +1,119 @@
#pragma once
#include <lvk/HelpersImGui.h>
#include <lvk/LVK.h>
#include <GLFW/glfw3.h>
#include <implot/implot.h>
#include <glm/ext.hpp>
#include <glm/glm.hpp>
#include <stb/stb_image.h>
#include <stb/stb_image_write.h>
#include "shared/UtilsFPS.h"
#include <shared/Bitmap.h>
#include <shared/Camera.h>
#include <shared/Graph.h>
#include <shared/Utils.h>
#include <shared/UtilsCubemap.h>
#include <functional>
using glm::mat3;
using glm::mat4;
using glm::vec2;
using glm::vec3;
using glm::vec4;
using DrawFrameFunc = std::function<void(uint32_t width, uint32_t height, float aspectRatio, float deltaSeconds)>;
struct GLTFMaterialIntro {
std::string name;
uint32_t materialMask;
uint32_t currentMaterialMask;
bool modified = false;
};
struct GLTFIntrospective {
std::vector<std::string> cameras;
uint32_t activeCamera = ~0u;
std::vector<std::string> animations;
std::vector<uint32_t> activeAnim;
std::vector<std::string> extensions;
std::vector<uint32_t> activeExtension;
std::vector<GLTFMaterialIntro> materials;
std::vector<bool> modifiedMaterial;
float blend = 0.5f;
bool showAnimations = false;
bool showAnimationBlend = false;
bool showCameras = false;
bool showMaterials = true;
};
struct VulkanAppConfig {
vec3 initialCameraPos = vec3(0.0f, 0.0f, -2.5f);
vec3 initialCameraTarget = vec3(0.0f, 0.0f, 0.0f);
bool showGLTFInspector = false;
};
class VulkanApp
{
public:
explicit VulkanApp(const VulkanAppConfig& cfg = {});
virtual ~VulkanApp();
virtual void run(DrawFrameFunc drawFrame);
virtual void drawGrid(
lvk::ICommandBuffer& buf, const mat4& proj, const vec3& origin = vec3(0.0f), uint32_t numSamples = 1,
lvk::Format colorFormat = lvk::Format_Invalid);
virtual void drawGrid(
lvk::ICommandBuffer& buf, const mat4& mvp, const vec3& origin, const vec3& camPos, uint32_t numSamples = 1,
lvk::Format colorFormat = lvk::Format_Invalid);
virtual void drawFPS();
virtual void drawMemo();
virtual void drawGTFInspector(GLTFIntrospective& intro);
virtual void drawGTFInspector_Animations(GLTFIntrospective& intro);
virtual void drawGTFInspector_Materials(GLTFIntrospective& intro);
virtual void drawGTFInspector_Cameras(GLTFIntrospective& intro);
lvk::Format getDepthFormat() const;
lvk::TextureHandle getDepthTexture() const { return depthTexture_; }
void addMouseButtonCallback(GLFWmousebuttonfun cb) { callbacksMouseButton.push_back(cb); }
void addKeyCallback(GLFWkeyfun cb) { callbacksKey.push_back(cb); }
public:
GLFWwindow* window_ = nullptr;
std::unique_ptr<lvk::IContext> ctx_;
lvk::Holder<lvk::TextureHandle> depthTexture_;
FramesPerSecondCounter fpsCounter_ = FramesPerSecondCounter(0.5f);
std::unique_ptr<lvk::ImGuiRenderer> imgui_;
ImPlotContext* implotCtx_ = nullptr;
const VulkanAppConfig cfg_ = {};
CameraPositioner_FirstPerson positioner_ = { cfg_.initialCameraPos, cfg_.initialCameraTarget, vec3(0.0f, 1.0f, 0.0f) };
Camera camera_ = Camera(positioner_);
struct MouseState {
vec2 pos = vec2(0.0f);
bool pressedLeft = false;
} mouseState_;
protected:
lvk::Holder<lvk::ShaderModuleHandle> gridVert = {};
lvk::Holder<lvk::ShaderModuleHandle> gridFrag = {};
lvk::Holder<lvk::RenderPipelineHandle> gridPipeline = {};
uint32_t pipelineSamples = 1;
std::vector<GLFWmousebuttonfun> callbacksMouseButton;
std::vector<GLFWkeyfun> callbacksKey;
};