mirror of
https://github.com/MihailRis/voxelcore.git
synced 2026-10-12 06:21:50 +00:00
Merge 9e9615c80b into dacebf94bc
This commit is contained in:
commit
2f884caddf
14 changed files with 212 additions and 279 deletions
|
|
@ -42,7 +42,7 @@ target_link_options(VoxelEngine PRIVATE $<$<CXX_COMPILER_ID:GNU>:-no-pie>)
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
TARGET VoxelEngine
|
TARGET VoxelEngine
|
||||||
POST_BUILD
|
POST_BUILD
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory_if_different
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/res $<TARGET_FILE_DIR:VoxelEngine>/res)
|
${CMAKE_CURRENT_SOURCE_DIR}/res $<TARGET_FILE_DIR:VoxelEngine>/res)
|
||||||
|
|
||||||
if(VOXELENGINE_BUILD_TESTS)
|
if(VOXELENGINE_BUILD_TESTS)
|
||||||
|
|
|
||||||
|
|
@ -138,5 +138,5 @@ VCA файл состоит из набора директив, порядок
|
||||||
|
|
||||||
|
|
||||||
>[!WARNING]
|
>[!WARNING]
|
||||||
> Использование незадокументированных функций может иметь иметь любые незадокументированные последствия.
|
> Использование незадокументированных функций может иметь любые незадокументированные последствия.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,13 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
|
||||||
find_package(OpenAL CONFIG REQUIRED)
|
find_package(OpenAL CONFIG REQUIRED)
|
||||||
else()
|
else()
|
||||||
find_package(OpenAL REQUIRED)
|
find_package(OpenAL REQUIRED)
|
||||||
|
if(NOT TARGET OpenAL::OpenAL)
|
||||||
|
add_library(OpenAL::OpenAL UNKNOWN IMPORTED)
|
||||||
|
set_target_properties(OpenAL::OpenAL PROPERTIES
|
||||||
|
IMPORTED_LOCATION "${OPENAL_LIBRARY}"
|
||||||
|
INTERFACE_INCLUDE_DIRECTORIES "${OPENAL_INCLUDE_DIR}"
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
find_package(ZLIB REQUIRED)
|
find_package(ZLIB REQUIRED)
|
||||||
find_package(PNG REQUIRED)
|
find_package(PNG REQUIRED)
|
||||||
|
|
|
||||||
|
|
@ -10,53 +10,69 @@ class ObjParser : BasicParser<char> {
|
||||||
std::vector<glm::vec2> uvs {{0, 0}};
|
std::vector<glm::vec2> uvs {{0, 0}};
|
||||||
std::vector<glm::vec3> normals {{0, 1, 0}};
|
std::vector<glm::vec3> normals {{0, 1, 0}};
|
||||||
|
|
||||||
// TODO: refactor
|
Vertex parseFaceVertex() {
|
||||||
void parseFace(Mesh& mesh) {
|
auto parseIndex = [&]() -> uint {
|
||||||
std::vector<Vertex> vertices;
|
if (!hasNext() || !is_digit(peekInLine())) {
|
||||||
while (hasNext()) {
|
return 0;
|
||||||
auto c = peekInLine();
|
}
|
||||||
if (c == '\n') {
|
return parseSimpleInt(10);
|
||||||
break;
|
};
|
||||||
} else if (hasNext()) {
|
|
||||||
uint indices[3] {};
|
|
||||||
uint i = 0;
|
|
||||||
do {
|
|
||||||
char next = peekInLine();
|
|
||||||
if (is_digit(next)) {
|
|
||||||
indices[i] = parseSimpleInt(10);
|
|
||||||
if (hasNext() && peekInLine() == '/') {
|
|
||||||
pos++;
|
|
||||||
}
|
|
||||||
} else if (next == '/') {
|
|
||||||
pos++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} while (hasNext() && peekInLine() != '\n' && ++i < 3);
|
|
||||||
|
|
||||||
vertices.push_back(Vertex {
|
uint coord = parseIndex();
|
||||||
coords[indices[0]], uvs[indices[1]], normals[indices[2]]});
|
uint uv = 0;
|
||||||
|
uint normal = 0;
|
||||||
|
|
||||||
|
if (hasNext() && peekInLine() == '/') {
|
||||||
|
pos++;
|
||||||
|
|
||||||
|
uv = parseIndex();
|
||||||
|
|
||||||
|
if (hasNext() && peekInLine() == '/') {
|
||||||
|
pos++;
|
||||||
|
normal = parseIndex();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hasNext() && peekInLine() != '\n') {
|
|
||||||
skipLine();
|
return Vertex {
|
||||||
}
|
coords[coord],
|
||||||
|
uvs[uv],
|
||||||
|
normals[normal]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
void triangulate(const std::vector<Vertex>& vertices, std::vector<Vertex>& output) {
|
||||||
if (vertices.size() >= 3) {
|
if (vertices.size() >= 3) {
|
||||||
for (size_t j = 0; j < vertices.size() - 2; j++) {
|
for (size_t j = 0; j < vertices.size() - 2; j++) {
|
||||||
mesh.vertices.push_back(vertices[0]);
|
output.push_back(vertices[0]);
|
||||||
for (size_t i = 1; i < 3; i++) {
|
for (size_t i = 1; i < 3; i++) {
|
||||||
mesh.vertices.push_back(vertices[i + j]);
|
output.push_back(vertices[i + j]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
public:
|
|
||||||
ObjParser(const std::string_view file, const std::string_view src)
|
void parseFace(Mesh& mesh) {
|
||||||
: BasicParser(file, src) {
|
std::vector<Vertex> vertices;
|
||||||
|
while (hasNext() && peekInLine() != '\n') {
|
||||||
|
vertices.push_back(parseFaceVertex());
|
||||||
|
}
|
||||||
|
triangulate(vertices, mesh.vertices);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<Model> parse() {
|
glm::vec3 parseVec3() {
|
||||||
// first iteration - collecting vertex data
|
float x = parseNumber().asNumber();
|
||||||
|
float y = parseNumber().asNumber();
|
||||||
|
float z = parseNumber().asNumber();
|
||||||
|
return glm::vec3 {x, y, z};
|
||||||
|
}
|
||||||
|
|
||||||
|
glm::vec2 parseVec2() {
|
||||||
|
float x = parseNumber().asNumber();
|
||||||
|
float y = parseNumber().asNumber();
|
||||||
|
return glm::vec2 {x, y};
|
||||||
|
}
|
||||||
|
|
||||||
|
void collectVertexData() {
|
||||||
while (hasNext()) {
|
while (hasNext()) {
|
||||||
if (peek() == '#') {
|
if (peek() == '#') {
|
||||||
skipLine();
|
skipLine();
|
||||||
|
|
@ -64,27 +80,18 @@ public:
|
||||||
}
|
}
|
||||||
auto cmd = parseName();
|
auto cmd = parseName();
|
||||||
if (cmd == "v") {
|
if (cmd == "v") {
|
||||||
float x = parseNumber().asNumber();
|
coords.emplace_back(parseVec3());
|
||||||
float y = parseNumber().asNumber();
|
|
||||||
float z = parseNumber().asNumber();
|
|
||||||
coords.emplace_back(x, y, z);
|
|
||||||
} else if (cmd == "vt") {
|
} else if (cmd == "vt") {
|
||||||
float u = parseNumber().asNumber();
|
uvs.emplace_back(parseVec2());
|
||||||
float v = parseNumber().asNumber();
|
|
||||||
uvs.emplace_back(u, v);
|
|
||||||
} else if (cmd == "vn") {
|
} else if (cmd == "vn") {
|
||||||
float x = parseNumber().asNumber();
|
normals.emplace_back(parseVec3());
|
||||||
float y = parseNumber().asNumber();
|
|
||||||
float z = parseNumber().asNumber();
|
|
||||||
normals.emplace_back(x, y, z);
|
|
||||||
} else {
|
} else {
|
||||||
skipLine();
|
skipLine();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// second iteration - building meshes
|
}
|
||||||
reset();
|
|
||||||
|
|
||||||
auto model = std::make_unique<Model>();
|
void buildMeshes(Model& model) {
|
||||||
std::string texture;
|
std::string texture;
|
||||||
while (hasNext()) {
|
while (hasNext()) {
|
||||||
if (peek() != '#' && parseName() == "usemtl") {
|
if (peek() != '#' && parseName() == "usemtl") {
|
||||||
|
|
@ -95,7 +102,7 @@ public:
|
||||||
skipLine();
|
skipLine();
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
Mesh* mesh = &model->addMesh(texture);
|
Mesh* mesh = &model.addMesh(texture);
|
||||||
while (hasNext()) {
|
while (hasNext()) {
|
||||||
if (peek() == '#') {
|
if (peek() == '#') {
|
||||||
skipLine();
|
skipLine();
|
||||||
|
|
@ -105,7 +112,7 @@ public:
|
||||||
if (cmd == "usemtl") {
|
if (cmd == "usemtl") {
|
||||||
skipWhitespace();
|
skipWhitespace();
|
||||||
texture = readUntilEOL();
|
texture = readUntilEOL();
|
||||||
mesh = &model->addMesh(texture);
|
mesh = &model.addMesh(texture);
|
||||||
break;
|
break;
|
||||||
} else if (cmd == "f") {
|
} else if (cmd == "f") {
|
||||||
parseFace(*mesh);
|
parseFace(*mesh);
|
||||||
|
|
@ -113,6 +120,19 @@ public:
|
||||||
skipLine();
|
skipLine();
|
||||||
}
|
}
|
||||||
} while (hasNext());
|
} while (hasNext());
|
||||||
|
}
|
||||||
|
public:
|
||||||
|
ObjParser(const std::string_view file, const std::string_view src)
|
||||||
|
: BasicParser(file, src) {
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Model> parse() {
|
||||||
|
// first iteration - collecting vertex data
|
||||||
|
collectVertexData();
|
||||||
|
// second iteration - building meshes
|
||||||
|
reset();
|
||||||
|
auto model = std::make_unique<Model>();
|
||||||
|
buildMeshes(*model);
|
||||||
model->clean();
|
model->clean();
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,143 +8,62 @@ using namespace advanced_pipeline;
|
||||||
|
|
||||||
static debug::Logger logger("gl-gbuffer");
|
static debug::Logger logger("gl-gbuffer");
|
||||||
|
|
||||||
// TODO: REFACTOR
|
void GBuffer::createBuffer(Buffer& buffer) {
|
||||||
|
if (buffer.buffer == 0) {
|
||||||
void GBuffer::createColorBuffer() {
|
if (&buffer == &depthBuffer)
|
||||||
if (colorBuffer == 0)
|
glGenRenderbuffers(1, &depthBuffer.buffer);
|
||||||
glGenTextures(1, &colorBuffer);
|
else
|
||||||
glBindTexture(GL_TEXTURE_2D, colorBuffer);
|
glGenTextures(1, &buffer.buffer);
|
||||||
glTexImage2D(
|
}
|
||||||
GL_TEXTURE_2D,
|
if (&buffer == &depthBuffer){
|
||||||
0,
|
glBindRenderbuffer(GL_RENDERBUFFER, depthBuffer.buffer);
|
||||||
GL_RGB,
|
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
|
||||||
width,
|
glFramebufferRenderbuffer(
|
||||||
height,
|
GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuffer.buffer
|
||||||
0,
|
);
|
||||||
GL_RGB,
|
} else {
|
||||||
GL_UNSIGNED_BYTE,
|
glBindTexture(GL_TEXTURE_2D, buffer.buffer);
|
||||||
nullptr
|
glTexImage2D(
|
||||||
);
|
GL_TEXTURE_2D,
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
0,
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
buffer.internalformat,
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
width,
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
height,
|
||||||
glFramebufferTexture2D(
|
0,
|
||||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBuffer, 0
|
buffer.format,
|
||||||
);
|
buffer.type,
|
||||||
|
nullptr
|
||||||
|
);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||||
|
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||||
|
if (&buffer != &ssaoBuffer)
|
||||||
|
{
|
||||||
|
glFramebufferTexture2D(
|
||||||
|
GL_FRAMEBUFFER, buffer.attachment, GL_TEXTURE_2D, buffer.buffer, 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void GBuffer::createPositionsBuffer() {
|
GBuffer::GBuffer(uint width, uint height)
|
||||||
if (positionsBuffer == 0)
|
: width(width), height(height),
|
||||||
glGenTextures(1, &positionsBuffer);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, positionsBuffer);
|
|
||||||
glTexImage2D(
|
|
||||||
GL_TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
GL_RGBA16F,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
0,
|
|
||||||
GL_RGBA,
|
|
||||||
GL_FLOAT,
|
|
||||||
nullptr
|
|
||||||
);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glFramebufferTexture2D(
|
|
||||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, positionsBuffer, 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GBuffer::createNormalsBuffer() {
|
colorBuffer {0, GL_RGB, GL_RGB, GL_UNSIGNED_BYTE, GL_COLOR_ATTACHMENT0},
|
||||||
if (normalsBuffer == 0)
|
positionsBuffer {0, GL_RGBA16F, GL_RGBA, GL_FLOAT, GL_COLOR_ATTACHMENT1},
|
||||||
glGenTextures(1, &normalsBuffer);
|
normalsBuffer {0, GL_RGBA16F, GL_RGB, GL_FLOAT, GL_COLOR_ATTACHMENT2},
|
||||||
glBindTexture(GL_TEXTURE_2D, normalsBuffer);
|
emissionBuffer {0, GL_R8, GL_RED, GL_FLOAT, GL_COLOR_ATTACHMENT3},
|
||||||
glTexImage2D(
|
depthBuffer {0, 0, 0, 0, 0},
|
||||||
GL_TEXTURE_2D,
|
ssaoBuffer {0, GL_R16F, GL_RED, GL_FLOAT, 0}
|
||||||
0,
|
{
|
||||||
GL_RGBA16F,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
0,
|
|
||||||
GL_RGB,
|
|
||||||
GL_FLOAT,
|
|
||||||
nullptr
|
|
||||||
);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glFramebufferTexture2D(
|
|
||||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, normalsBuffer, 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GBuffer::createEmissionBuffer() {
|
|
||||||
if (emissionBuffer == 0)
|
|
||||||
glGenTextures(1, &emissionBuffer);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, emissionBuffer);
|
|
||||||
glTexImage2D(
|
|
||||||
GL_TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
GL_R8,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
0,
|
|
||||||
GL_RED,
|
|
||||||
GL_FLOAT,
|
|
||||||
nullptr
|
|
||||||
);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
glFramebufferTexture2D(
|
|
||||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT3, GL_TEXTURE_2D, emissionBuffer, 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GBuffer::createDepthBuffer() {
|
|
||||||
if (depthBuffer == 0)
|
|
||||||
glGenRenderbuffers(1, &depthBuffer);
|
|
||||||
glBindRenderbuffer(GL_RENDERBUFFER, depthBuffer);
|
|
||||||
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT24, width, height);
|
|
||||||
glFramebufferRenderbuffer(
|
|
||||||
GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthBuffer
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void GBuffer::createSSAOBuffer() {
|
|
||||||
if (ssaoBuffer == 0)
|
|
||||||
glGenTextures(1, &ssaoBuffer);
|
|
||||||
glBindTexture(GL_TEXTURE_2D, ssaoBuffer);
|
|
||||||
glTexImage2D(
|
|
||||||
GL_TEXTURE_2D,
|
|
||||||
0,
|
|
||||||
GL_R16F,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
0,
|
|
||||||
GL_RED,
|
|
||||||
GL_FLOAT,
|
|
||||||
nullptr
|
|
||||||
);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
|
||||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
GBuffer::GBuffer(uint width, uint height) : width(width), height(height) {
|
|
||||||
glGenFramebuffers(1, &fbo);
|
glGenFramebuffers(1, &fbo);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
|
||||||
createColorBuffer();
|
createBuffer(colorBuffer);
|
||||||
createPositionsBuffer();
|
createBuffer(positionsBuffer);
|
||||||
createNormalsBuffer();
|
createBuffer(normalsBuffer);
|
||||||
createEmissionBuffer();
|
createBuffer(emissionBuffer);
|
||||||
|
|
||||||
GLenum attachments[4] = {
|
GLenum attachments[4] = {
|
||||||
GL_COLOR_ATTACHMENT0,
|
GL_COLOR_ATTACHMENT0,
|
||||||
|
|
@ -154,7 +73,7 @@ GBuffer::GBuffer(uint width, uint height) : width(width), height(height) {
|
||||||
};
|
};
|
||||||
glDrawBuffers(4, attachments);
|
glDrawBuffers(4, attachments);
|
||||||
|
|
||||||
createDepthBuffer();
|
createBuffer(depthBuffer);
|
||||||
|
|
||||||
int status;
|
int status;
|
||||||
if ((status = glCheckFramebufferStatus(GL_FRAMEBUFFER)) != GL_FRAMEBUFFER_COMPLETE) {
|
if ((status = glCheckFramebufferStatus(GL_FRAMEBUFFER)) != GL_FRAMEBUFFER_COMPLETE) {
|
||||||
|
|
@ -164,9 +83,9 @@ GBuffer::GBuffer(uint width, uint height) : width(width), height(height) {
|
||||||
|
|
||||||
glGenFramebuffers(1, &ssaoFbo);
|
glGenFramebuffers(1, &ssaoFbo);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFbo);
|
||||||
createSSAOBuffer();
|
createBuffer(ssaoBuffer);
|
||||||
glFramebufferTexture2D(
|
glFramebufferTexture2D(
|
||||||
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoBuffer, 0
|
GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, ssaoBuffer.buffer, 0
|
||||||
);
|
);
|
||||||
GLenum ssaoAttachments[1] = {GL_COLOR_ATTACHMENT0};
|
GLenum ssaoAttachments[1] = {GL_COLOR_ATTACHMENT0};
|
||||||
glDrawBuffers(1, ssaoAttachments);
|
glDrawBuffers(1, ssaoAttachments);
|
||||||
|
|
@ -179,12 +98,12 @@ GBuffer::GBuffer(uint width, uint height) : width(width), height(height) {
|
||||||
}
|
}
|
||||||
|
|
||||||
GBuffer::~GBuffer() {
|
GBuffer::~GBuffer() {
|
||||||
glDeleteTextures(1, &colorBuffer);
|
glDeleteTextures(1, &colorBuffer.buffer);
|
||||||
glDeleteTextures(1, &positionsBuffer);
|
glDeleteTextures(1, &positionsBuffer.buffer);
|
||||||
glDeleteTextures(1, &normalsBuffer);
|
glDeleteTextures(1, &normalsBuffer.buffer);
|
||||||
glDeleteTextures(1, &emissionBuffer);
|
glDeleteTextures(1, &emissionBuffer.buffer);
|
||||||
glDeleteTextures(1, &ssaoBuffer);
|
glDeleteTextures(1, &ssaoBuffer.buffer);
|
||||||
glDeleteRenderbuffers(1, &depthBuffer);
|
glDeleteRenderbuffers(1, &depthBuffer.buffer);
|
||||||
glDeleteFramebuffers(1, &fbo);
|
glDeleteFramebuffers(1, &fbo);
|
||||||
glDeleteFramebuffers(1, &ssaoFbo);
|
glDeleteFramebuffers(1, &ssaoFbo);
|
||||||
}
|
}
|
||||||
|
|
@ -204,23 +123,23 @@ void GBuffer::unbind() const {
|
||||||
|
|
||||||
void GBuffer::bindBuffers() const {
|
void GBuffer::bindBuffers() const {
|
||||||
glActiveTexture(GL_TEXTURE0 + TARGET_EMISSION);
|
glActiveTexture(GL_TEXTURE0 + TARGET_EMISSION);
|
||||||
glBindTexture(GL_TEXTURE_2D, emissionBuffer);
|
glBindTexture(GL_TEXTURE_2D, emissionBuffer.buffer);
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0 + TARGET_NORMALS);
|
glActiveTexture(GL_TEXTURE0 + TARGET_NORMALS);
|
||||||
glBindTexture(GL_TEXTURE_2D, normalsBuffer);
|
glBindTexture(GL_TEXTURE_2D, normalsBuffer.buffer);
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0 + TARGET_POSITIONS);
|
glActiveTexture(GL_TEXTURE0 + TARGET_POSITIONS);
|
||||||
glBindTexture(GL_TEXTURE_2D, positionsBuffer);
|
glBindTexture(GL_TEXTURE_2D, positionsBuffer.buffer);
|
||||||
|
|
||||||
glActiveTexture(GL_TEXTURE0 + TARGET_COLOR);
|
glActiveTexture(GL_TEXTURE0 + TARGET_COLOR);
|
||||||
glBindTexture(GL_TEXTURE_2D, colorBuffer);
|
glBindTexture(GL_TEXTURE_2D, colorBuffer.buffer);
|
||||||
|
|
||||||
if (TARGET_COLOR != 0)
|
if (TARGET_COLOR != 0)
|
||||||
glActiveTexture(GL_TEXTURE0);
|
glActiveTexture(GL_TEXTURE0);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GBuffer::bindSSAOBuffer() const {
|
void GBuffer::bindSSAOBuffer() const {
|
||||||
glBindTexture(GL_TEXTURE_2D, ssaoBuffer);
|
glBindTexture(GL_TEXTURE_2D, ssaoBuffer.buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
void GBuffer::bindDepthBuffer(int drawFbo) {
|
void GBuffer::bindDepthBuffer(int drawFbo) {
|
||||||
|
|
@ -239,24 +158,24 @@ void GBuffer::resize(uint width, uint height) {
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
|
||||||
|
|
||||||
createDepthBuffer();
|
createBuffer(depthBuffer);
|
||||||
createColorBuffer();
|
createBuffer(colorBuffer);
|
||||||
createPositionsBuffer();
|
createBuffer(positionsBuffer);
|
||||||
createNormalsBuffer();
|
createBuffer(normalsBuffer);
|
||||||
createEmissionBuffer();
|
createBuffer(emissionBuffer);
|
||||||
|
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
|
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFbo);
|
glBindFramebuffer(GL_FRAMEBUFFER, ssaoFbo);
|
||||||
createSSAOBuffer();
|
createBuffer(ssaoBuffer);
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
glBindFramebuffer(GL_FRAMEBUFFER, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<ImageData> GBuffer::toImage() const {
|
std::unique_ptr<ImageData> GBuffer::toImage() const {
|
||||||
auto data = std::make_unique<ubyte[]>(width * height * 3);
|
auto data = std::make_unique<ubyte[]>(width * height * 3);
|
||||||
glBindTexture(GL_TEXTURE_2D, colorBuffer);
|
glBindTexture(GL_TEXTURE_2D, colorBuffer.buffer);
|
||||||
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, data.get());
|
glGetTexImage(GL_TEXTURE_2D, 0, GL_RGB, GL_UNSIGNED_BYTE, data.get());
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
return std::make_unique<ImageData>(
|
return std::make_unique<ImageData>(
|
||||||
|
|
|
||||||
|
|
@ -25,22 +25,25 @@ public:
|
||||||
|
|
||||||
std::unique_ptr<ImageData> toImage() const;
|
std::unique_ptr<ImageData> toImage() const;
|
||||||
private:
|
private:
|
||||||
|
struct Buffer {
|
||||||
|
uint buffer;
|
||||||
|
const int internalformat;
|
||||||
|
const uint format;
|
||||||
|
const uint type;
|
||||||
|
const uint attachment;
|
||||||
|
};
|
||||||
|
|
||||||
uint width;
|
uint width;
|
||||||
uint height;
|
uint height;
|
||||||
|
|
||||||
uint fbo;
|
uint fbo;
|
||||||
uint colorBuffer = 0;
|
Buffer colorBuffer;
|
||||||
uint positionsBuffer = 0;
|
Buffer positionsBuffer;
|
||||||
uint normalsBuffer = 0;
|
Buffer normalsBuffer;
|
||||||
uint emissionBuffer = 0;
|
Buffer emissionBuffer;
|
||||||
uint depthBuffer = 0;
|
Buffer depthBuffer;
|
||||||
uint ssaoFbo = 0;
|
uint ssaoFbo;
|
||||||
uint ssaoBuffer = 0;
|
Buffer ssaoBuffer;
|
||||||
|
|
||||||
void createColorBuffer();
|
void createBuffer(Buffer& buffer);
|
||||||
void createPositionsBuffer();
|
|
||||||
void createNormalsBuffer();
|
|
||||||
void createEmissionBuffer();
|
|
||||||
void createDepthBuffer();
|
|
||||||
void createSSAOBuffer();
|
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
#include "PostProcessing.hpp"
|
#include "PostProcessing.hpp"
|
||||||
|
|
||||||
|
#include <GL/glew.h>
|
||||||
|
|
||||||
#include "Mesh.hpp"
|
#include "Mesh.hpp"
|
||||||
#include "Shader.hpp"
|
#include "Shader.hpp"
|
||||||
#include "GBuffer.hpp"
|
#include "GBuffer.hpp"
|
||||||
|
|
@ -18,7 +21,7 @@ using namespace advanced_pipeline;
|
||||||
|
|
||||||
PostProcessing::PostProcessing(size_t effectSlotsCount)
|
PostProcessing::PostProcessing(size_t effectSlotsCount)
|
||||||
: effectSlots(effectSlotsCount) {
|
: effectSlots(effectSlotsCount) {
|
||||||
// Fullscreen quad mesh bulding
|
// Fullscreen quad mesh building
|
||||||
PostProcessingVertex meshData[] {
|
PostProcessingVertex meshData[] {
|
||||||
{{-1.0f, -1.0f}},
|
{{-1.0f, -1.0f}},
|
||||||
{{-1.0f, 1.0f}},
|
{{-1.0f, 1.0f}},
|
||||||
|
|
@ -49,7 +52,9 @@ PostProcessing::PostProcessing(size_t effectSlotsCount)
|
||||||
glBindTexture(GL_TEXTURE_2D, 0);
|
glBindTexture(GL_TEXTURE_2D, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
PostProcessing::~PostProcessing() = default;
|
PostProcessing::~PostProcessing() {
|
||||||
|
glDeleteTextures(1, &noiseTexture);
|
||||||
|
}
|
||||||
|
|
||||||
void PostProcessing::use(DrawContext& context, bool gbufferPipeline) {
|
void PostProcessing::use(DrawContext& context, bool gbufferPipeline) {
|
||||||
const auto& vp = context.getViewport();
|
const auto& vp = context.getViewport();
|
||||||
|
|
@ -105,8 +110,9 @@ void PostProcessing::configureEffect(
|
||||||
shader.uniform3f("u_cameraPos", camera.position);
|
shader.uniform3f("u_cameraPos", camera.position);
|
||||||
shader.uniform1f("u_timer", timer);
|
shader.uniform1f("u_timer", timer);
|
||||||
shader.uniformMatrix("u_projection", camera.getProjection());
|
shader.uniformMatrix("u_projection", camera.getProjection());
|
||||||
shader.uniformMatrix("u_view", camera.getView());
|
const auto view = camera.getView();
|
||||||
shader.uniformMatrix("u_inverseView", glm::inverse(camera.getView()));
|
shader.uniformMatrix("u_view", view);
|
||||||
|
shader.uniformMatrix("u_inverseView", glm::inverse(view));
|
||||||
}
|
}
|
||||||
|
|
||||||
void PostProcessing::renderDeferredShading(
|
void PostProcessing::renderDeferredShading(
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ struct PostProcessingVertex {
|
||||||
};
|
};
|
||||||
|
|
||||||
/// @brief Framebuffer with blitting with shaders.
|
/// @brief Framebuffer with blitting with shaders.
|
||||||
/// @attention Current implementation does not support multiple render passes
|
|
||||||
/// for multiple effects. Will be implemented in v0.21
|
|
||||||
class PostProcessing {
|
class PostProcessing {
|
||||||
public:
|
public:
|
||||||
PostProcessing(size_t effectSlotsCount);
|
PostProcessing(size_t effectSlotsCount);
|
||||||
|
|
@ -73,12 +71,12 @@ private:
|
||||||
|
|
||||||
void refreshFbos(uint width, uint height);
|
void refreshFbos(uint width, uint height);
|
||||||
|
|
||||||
/// @brief Main framebuffer (lasy field)
|
/// @brief Main framebuffer (lazy field)
|
||||||
std::unique_ptr<Framebuffer> fbo;
|
std::unique_ptr<Framebuffer> fbo;
|
||||||
std::unique_ptr<Framebuffer> fboSecond;
|
std::unique_ptr<Framebuffer> fboSecond;
|
||||||
/// @brief Fullscreen quad mesh as the post-processing canvas
|
/// @brief Fullscreen quad mesh as the post-processing canvas
|
||||||
std::unique_ptr<Mesh<PostProcessingVertex>> quadMesh;
|
std::unique_ptr<Mesh<PostProcessingVertex>> quadMesh;
|
||||||
std::vector<std::shared_ptr<PostEffect>> effectSlots;
|
std::vector<std::shared_ptr<PostEffect>> effectSlots;
|
||||||
std::unique_ptr<GBuffer> gbuffer;
|
std::unique_ptr<GBuffer> gbuffer;
|
||||||
uint noiseTexture;
|
uint noiseTexture = 0;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -324,7 +324,8 @@ void WorldRenderer::renderInWorldLines(
|
||||||
auto ctx = pctx.sub(lineBatch.get());
|
auto ctx = pctx.sub(lineBatch.get());
|
||||||
bool culling = engine.getSettings().graphics.frustumCulling.get();
|
bool culling = engine.getSettings().graphics.frustumCulling.get();
|
||||||
level.entities->renderDebug(
|
level.entities->renderDebug(
|
||||||
*lineBatch, culling ? frustumCulling.get() : nullptr, ctx
|
*lineBatch, culling ? frustumCulling.get() : nullptr, ctx,
|
||||||
|
player.currentCamera.get() == player.fpCamera.get() ? player.getEntity() : 0
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
#include "GUI.hpp"
|
#include "GUI.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <utility>
|
|
||||||
|
|
||||||
#include "assets/Assets.hpp"
|
#include "assets/Assets.hpp"
|
||||||
#include "debug/Logger.hpp"
|
#include "debug/Logger.hpp"
|
||||||
#include "elements/Label.hpp"
|
#include "elements/Label.hpp"
|
||||||
|
|
@ -17,7 +14,6 @@
|
||||||
#include "graphics/core/Shader.hpp"
|
#include "graphics/core/Shader.hpp"
|
||||||
#include "graphics/core/Font.hpp"
|
#include "graphics/core/Font.hpp"
|
||||||
#include "graphics/core/DrawContext.hpp"
|
#include "graphics/core/DrawContext.hpp"
|
||||||
#include "graphics/core/Shader.hpp"
|
|
||||||
#include "gui_util.hpp"
|
#include "gui_util.hpp"
|
||||||
#include "window/Camera.hpp"
|
#include "window/Camera.hpp"
|
||||||
#include "window/Window.hpp"
|
#include "window/Window.hpp"
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,9 @@ static int l_get_def(lua::State* L) {
|
||||||
static int l_spawn(lua::State* L) {
|
static int l_spawn(lua::State* L) {
|
||||||
auto level = controller->getLevel();
|
auto level = controller->getLevel();
|
||||||
auto defname = lua::tostring(L, 1);
|
auto defname = lua::tostring(L, 1);
|
||||||
|
if (defname == nullptr) {
|
||||||
|
throw std::runtime_error("string expected, got nil");
|
||||||
|
}
|
||||||
auto& def = content->entities.require(defname);
|
auto& def = content->entities.require(defname);
|
||||||
auto pos = lua::tovec3(L, 2);
|
auto pos = lua::tovec3(L, 2);
|
||||||
dv::value args = nullptr;
|
dv::value args = nullptr;
|
||||||
|
|
|
||||||
|
|
@ -211,59 +211,36 @@ static int l_tostring(lua::State* L) {
|
||||||
return lua::pushstring(L, ss.str());
|
return lua::pushstring(L, ss.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#define VECLIB_BASE(n) \
|
||||||
|
{"add", lua::wrap<l_binop<n, std::plus>>}, \
|
||||||
|
{"sub", lua::wrap<l_binop<n, std::minus>>}, \
|
||||||
|
{"mul", lua::wrap<l_binop<n, std::multiplies>>}, \
|
||||||
|
{"div", lua::wrap<l_binop<n, std::divides>>}, \
|
||||||
|
{"distance", lua::wrap<l_distance<n>>}, \
|
||||||
|
{"normalize", lua::wrap<l_unaryop<n, glm::normalize>>}, \
|
||||||
|
{"length", lua::wrap<l_scalar_op<n, glm::length>>}, \
|
||||||
|
{"tostring", lua::wrap<l_tostring<n>>}, \
|
||||||
|
{"abs", lua::wrap<l_unaryop<n, glm::abs>>}, \
|
||||||
|
{"round", lua::wrap<l_unaryop<n, glm::round>>}, \
|
||||||
|
{"inverse", lua::wrap<l_inverse<n>>}, \
|
||||||
|
{"pow", lua::wrap<l_pow<n>>}, \
|
||||||
|
{"dot", lua::wrap<l_dot<n>>}, \
|
||||||
|
{"mix", lua::wrap<l_mix<n>>},
|
||||||
|
|
||||||
const luaL_Reg vec2lib[] = {
|
const luaL_Reg vec2lib[] = {
|
||||||
{"add", lua::wrap<l_binop<2, std::plus>>},
|
VECLIB_BASE(2)
|
||||||
{"sub", lua::wrap<l_binop<2, std::minus>>},
|
|
||||||
{"mul", lua::wrap<l_binop<2, std::multiplies>>},
|
|
||||||
{"div", lua::wrap<l_binop<2, std::divides>>},
|
|
||||||
{"distance", lua::wrap<l_distance<2>>},
|
|
||||||
{"normalize", lua::wrap<l_unaryop<2, glm::normalize>>},
|
|
||||||
{"length", lua::wrap<l_scalar_op<2, glm::length>>},
|
|
||||||
{"tostring", lua::wrap<l_tostring<2>>},
|
|
||||||
{"abs", lua::wrap<l_unaryop<2, glm::abs>>},
|
|
||||||
{"round", lua::wrap<l_unaryop<2, glm::round>>},
|
|
||||||
{"inverse", lua::wrap<l_inverse<2>>},
|
|
||||||
{"pow", lua::wrap<l_pow<2>>},
|
|
||||||
{"dot", lua::wrap<l_dot<2>>},
|
|
||||||
{"angle", lua::wrap<l_vec2_angle>},
|
{"angle", lua::wrap<l_vec2_angle>},
|
||||||
{"mix", lua::wrap<l_mix<2>>},
|
|
||||||
{"rotate", lua::wrap<l_vec2_rotate>},
|
{"rotate", lua::wrap<l_vec2_rotate>},
|
||||||
{nullptr, nullptr}
|
{nullptr, nullptr}
|
||||||
};
|
};
|
||||||
|
|
||||||
const luaL_Reg vec3lib[] = {
|
const luaL_Reg vec3lib[] = {
|
||||||
{"add", lua::wrap<l_binop<3, std::plus>>},
|
VECLIB_BASE(3)
|
||||||
{"sub", lua::wrap<l_binop<3, std::minus>>},
|
|
||||||
{"mul", lua::wrap<l_binop<3, std::multiplies>>},
|
|
||||||
{"div", lua::wrap<l_binop<3, std::divides>>},
|
|
||||||
{"distance", lua::wrap<l_distance<3>>},
|
|
||||||
{"normalize", lua::wrap<l_unaryop<3, glm::normalize>>},
|
|
||||||
{"length", lua::wrap<l_scalar_op<3, glm::length>>},
|
|
||||||
{"tostring", lua::wrap<l_tostring<3>>},
|
|
||||||
{"abs", lua::wrap<l_unaryop<3, glm::abs>>},
|
|
||||||
{"round", lua::wrap<l_unaryop<3, glm::round>>},
|
|
||||||
{"inverse", lua::wrap<l_inverse<3>>},
|
|
||||||
{"pow", lua::wrap<l_pow<3>>},
|
|
||||||
{"dot", lua::wrap<l_dot<3>>},
|
|
||||||
{"spherical_rand", lua::wrap<l_spherical_rand>},
|
{"spherical_rand", lua::wrap<l_spherical_rand>},
|
||||||
{"mix", lua::wrap<l_mix<3>>},
|
|
||||||
{nullptr, nullptr}
|
{nullptr, nullptr}
|
||||||
};
|
};
|
||||||
|
|
||||||
const luaL_Reg vec4lib[] = {
|
const luaL_Reg vec4lib[] = {
|
||||||
{"add", lua::wrap<l_binop<4, std::plus>>},
|
VECLIB_BASE(4)
|
||||||
{"sub", lua::wrap<l_binop<4, std::minus>>},
|
|
||||||
{"mul", lua::wrap<l_binop<4, std::multiplies>>},
|
|
||||||
{"div", lua::wrap<l_binop<4, std::divides>>},
|
|
||||||
{"distance", lua::wrap<l_distance<4>>},
|
|
||||||
{"normalize", lua::wrap<l_unaryop<4, glm::normalize>>},
|
|
||||||
{"length", lua::wrap<l_scalar_op<4, glm::length>>},
|
|
||||||
{"tostring", lua::wrap<l_tostring<4>>},
|
|
||||||
{"abs", lua::wrap<l_unaryop<4, glm::abs>>},
|
|
||||||
{"round", lua::wrap<l_unaryop<4, glm::round>>},
|
|
||||||
{"inverse", lua::wrap<l_inverse<4>>},
|
|
||||||
{"pow", lua::wrap<l_pow<4>>},
|
|
||||||
{"dot", lua::wrap<l_dot<4>>},
|
|
||||||
{"mix", lua::wrap<l_mix<4>>},
|
|
||||||
{nullptr, nullptr}
|
{nullptr, nullptr}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -422,7 +422,7 @@ static void debug_render_skeleton(
|
||||||
}
|
}
|
||||||
|
|
||||||
void Entities::renderDebug(
|
void Entities::renderDebug(
|
||||||
LineBatch& batch, const Frustum* frustum, const DrawContext& pctx
|
LineBatch& batch, const Frustum* frustum, const DrawContext& pctx, entityid_t fpsEntity
|
||||||
) {
|
) {
|
||||||
{
|
{
|
||||||
auto ctx = pctx.sub(&batch);
|
auto ctx = pctx.sub(&batch);
|
||||||
|
|
@ -450,12 +450,15 @@ void Entities::renderDebug(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
auto view = registry->view<Transform, rigging::Skeleton>();
|
auto view = registry->view<EntityId, Transform, rigging::Skeleton>();
|
||||||
auto ctx = pctx.sub(&batch);
|
auto ctx = pctx.sub(&batch);
|
||||||
ctx.setDepthTest(false);
|
ctx.setDepthTest(false);
|
||||||
ctx.setDepthMask(false);
|
ctx.setDepthMask(false);
|
||||||
ctx.setLineWidth(2);
|
ctx.setLineWidth(2);
|
||||||
for (auto [entity, transform, skeleton] : view.each()) {
|
for (auto [entity, eid, transform, skeleton] : view.each()) {
|
||||||
|
if (eid.uid == fpsEntity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
auto config = skeleton.config;
|
auto config = skeleton.config;
|
||||||
if (config == nullptr) {
|
if (config == nullptr) {
|
||||||
continue;
|
continue;
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ public:
|
||||||
void update(float delta);
|
void update(float delta);
|
||||||
|
|
||||||
void renderDebug(
|
void renderDebug(
|
||||||
LineBatch& batch, const Frustum* frustum, const DrawContext& ctx
|
LineBatch& batch, const Frustum* frustum, const DrawContext& ctx, entityid_t fpsEntity
|
||||||
);
|
);
|
||||||
void render(
|
void render(
|
||||||
const Assets& assets,
|
const Assets& assets,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue