ADD: Normal mapping
This commit is contained in:
219
src/scene/tangent_space.cpp
Normal file
219
src/scene/tangent_space.cpp
Normal file
@@ -0,0 +1,219 @@
|
||||
#include "tangent_space.h"
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/epsilon.hpp>
|
||||
#include <algorithm>
|
||||
|
||||
#include "glm/gtx/norm.hpp"
|
||||
|
||||
namespace
|
||||
{
|
||||
struct Range
|
||||
{
|
||||
size_t indexStart;
|
||||
size_t indexCount;
|
||||
size_t vertexStart;
|
||||
size_t vertexCount;
|
||||
};
|
||||
|
||||
static inline glm::vec3 orthonormal_tangent(const glm::vec3 &n)
|
||||
{
|
||||
// Build tangent orthogonal to n from an arbitrary axis
|
||||
glm::vec3 a = (std::abs(n.z) < 0.999f) ? glm::vec3(0, 0, 1) : glm::vec3(0, 1, 0);
|
||||
glm::vec3 t = glm::normalize(glm::cross(a, n));
|
||||
return t;
|
||||
}
|
||||
|
||||
static void generate_fallback(std::vector<Vertex> &vtx, const Range &r)
|
||||
{
|
||||
for (size_t i = 0; i < r.vertexCount; ++i)
|
||||
{
|
||||
Vertex &v = vtx[r.vertexStart + i];
|
||||
glm::vec3 T = orthonormal_tangent(glm::normalize(v.normal));
|
||||
v.tangent = glm::vec4(T, 1.0f);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
namespace geom
|
||||
{
|
||||
#ifdef MIKKTS_ENABLE
|
||||
#include <mikktspace.h>
|
||||
|
||||
struct MikkAdapter
|
||||
{
|
||||
std::vector<Vertex> *verts;
|
||||
const std::vector<uint32_t> *inds;
|
||||
Range range;
|
||||
};
|
||||
|
||||
static int mikk_get_num_faces(const SMikkTSpaceContext *ctx)
|
||||
{
|
||||
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
|
||||
return static_cast<int>(ad->range.indexCount / 3);
|
||||
}
|
||||
|
||||
static int mikk_get_num_verts_of_face(const SMikkTSpaceContext *, const int /*face*/) { return 3; }
|
||||
|
||||
static void mikk_get_position(const SMikkTSpaceContext *ctx, float outpos[], const int face, const int vert)
|
||||
{
|
||||
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
|
||||
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
|
||||
const Vertex &v = ad->verts->at(idx);
|
||||
outpos[0] = v.position.x;
|
||||
outpos[1] = v.position.y;
|
||||
outpos[2] = v.position.z;
|
||||
}
|
||||
|
||||
static void mikk_get_normal(const SMikkTSpaceContext *ctx, float outnormal[], const int face, const int vert)
|
||||
{
|
||||
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
|
||||
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
|
||||
const Vertex &v = ad->verts->at(idx);
|
||||
outnormal[0] = v.normal.x;
|
||||
outnormal[1] = v.normal.y;
|
||||
outnormal[2] = v.normal.z;
|
||||
}
|
||||
|
||||
static void mikk_get_texcoord(const SMikkTSpaceContext *ctx, float outuv[], const int face, const int vert)
|
||||
{
|
||||
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
|
||||
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
|
||||
const Vertex &v = ad->verts->at(idx);
|
||||
outuv[0] = v.uv_x;
|
||||
outuv[1] = v.uv_y;
|
||||
}
|
||||
|
||||
static void mikk_set_tspace_basic(const SMikkTSpaceContext *ctx, const float tangent[], const float sign,
|
||||
const int face, const int vert)
|
||||
{
|
||||
const MikkAdapter *ad = reinterpret_cast<const MikkAdapter *>(ctx->m_pUserData);
|
||||
uint32_t idx = ad->inds->at(ad->range.indexStart + face * 3 + vert);
|
||||
Vertex &v = ad->verts->at(idx);
|
||||
v.tangent = glm::vec4(tangent[0], tangent[1], tangent[2], sign);
|
||||
}
|
||||
|
||||
static bool generate_mikk(std::vector<Vertex> &vertices, const std::vector<uint32_t> &indices, const Range &r)
|
||||
{
|
||||
SMikkTSpaceInterface iface{};
|
||||
iface.m_getNumFaces = mikk_get_num_faces;
|
||||
iface.m_getNumVerticesOfFace = mikk_get_num_verts_of_face;
|
||||
iface.m_getPosition = mikk_get_position;
|
||||
iface.m_getNormal = mikk_get_normal;
|
||||
iface.m_getTexCoord = mikk_get_texcoord;
|
||||
iface.m_setTSpaceBasic = mikk_set_tspace_basic;
|
||||
|
||||
MikkAdapter ad{&vertices, &indices, r};
|
||||
SMikkTSpaceContext ctx{};
|
||||
ctx.m_pInterface = &iface;
|
||||
ctx.m_pUserData = &ad;
|
||||
|
||||
// angle weighting, respect vtx-deriv continuity by default
|
||||
return genTangSpaceDefault(&ctx) != 0;
|
||||
}
|
||||
#endif // MIKKTS_ENABLE
|
||||
|
||||
void generate_tangents_range(std::vector<Vertex> &vertices, const std::vector<uint32_t> &indices,
|
||||
size_t indexStart, size_t indexCount,
|
||||
size_t vertexStart, size_t vertexCount)
|
||||
{
|
||||
Range r{indexStart, indexCount, vertexStart, vertexCount};
|
||||
|
||||
if (indexCount < 3 || vertexCount == 0)
|
||||
{
|
||||
generate_fallback(vertices, r);
|
||||
return;
|
||||
}
|
||||
|
||||
#ifdef MIKKTS_ENABLE
|
||||
if (generate_mikk(vertices, indices, r))
|
||||
{
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
std::vector<glm::vec3> tan1(vertexCount, glm::vec3(0.0f));
|
||||
std::vector<glm::vec3> bit1(vertexCount, glm::vec3(0.0f));
|
||||
|
||||
bool anyValid = false;
|
||||
for (size_t it = 0; it + 2 < indexCount; it += 3)
|
||||
{
|
||||
uint32_t i0 = indices[r.indexStart + it];
|
||||
uint32_t i1 = indices[r.indexStart + it + 1];
|
||||
uint32_t i2 = indices[r.indexStart + it + 2];
|
||||
|
||||
// guard against out of range
|
||||
if (i0 < r.vertexStart || i1 < r.vertexStart || i2 < r.vertexStart) continue;
|
||||
if (i0 >= r.vertexStart + r.vertexCount || i1 >= r.vertexStart + r.vertexCount || i2 >= r.vertexStart + r.
|
||||
vertexCount) continue;
|
||||
|
||||
const Vertex &v0 = vertices[i0];
|
||||
const Vertex &v1 = vertices[i1];
|
||||
const Vertex &v2 = vertices[i2];
|
||||
|
||||
glm::vec3 p0 = v0.position;
|
||||
glm::vec3 p1 = v1.position;
|
||||
glm::vec3 p2 = v2.position;
|
||||
|
||||
glm::vec2 w0{v0.uv_x, v0.uv_y};
|
||||
glm::vec2 w1{v1.uv_x, v1.uv_y};
|
||||
glm::vec2 w2{v2.uv_x, v2.uv_y};
|
||||
|
||||
glm::vec3 e1 = p1 - p0;
|
||||
glm::vec3 e2 = p2 - p0;
|
||||
glm::vec2 d1 = w1 - w0;
|
||||
glm::vec2 d2 = w2 - w0;
|
||||
|
||||
float denom = d1.x * d2.y - d1.y * d2.x;
|
||||
if (std::abs(denom) < 1e-8f)
|
||||
{
|
||||
continue; // degenerate UV mapping; skip this tri
|
||||
}
|
||||
anyValid = true;
|
||||
float rcp = 1.0f / denom;
|
||||
glm::vec3 t = (e1 * d2.y - e2 * d1.y) * rcp;
|
||||
glm::vec3 b = (-e1 * d2.x + e2 * d1.x) * rcp;
|
||||
|
||||
size_t l0 = i0 - r.vertexStart;
|
||||
size_t l1 = i1 - r.vertexStart;
|
||||
size_t l2 = i2 - r.vertexStart;
|
||||
tan1[l0] += t;
|
||||
tan1[l1] += t;
|
||||
tan1[l2] += t;
|
||||
bit1[l0] += b;
|
||||
bit1[l1] += b;
|
||||
bit1[l2] += b;
|
||||
}
|
||||
|
||||
if (!anyValid)
|
||||
{
|
||||
generate_fallback(vertices, r);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < r.vertexCount; ++i)
|
||||
{
|
||||
Vertex &v = vertices[r.vertexStart + i];
|
||||
glm::vec3 N = glm::normalize(v.normal);
|
||||
glm::vec3 T = tan1[i];
|
||||
glm::vec3 B = bit1[i];
|
||||
if (glm::length2(T) < 1e-16f)
|
||||
{
|
||||
T = orthonormal_tangent(N);
|
||||
v.tangent = glm::vec4(T, 1.0f);
|
||||
continue;
|
||||
}
|
||||
// Gram-Schmidt orthonormalize
|
||||
T = glm::normalize(T - N * glm::dot(N, T));
|
||||
// Compute handedness
|
||||
float w = (glm::dot(glm::cross(N, T), B) < 0.0f) ? -1.0f : 1.0f;
|
||||
v.tangent = glm::vec4(T, w);
|
||||
}
|
||||
}
|
||||
|
||||
void generate_tangents(std::vector<Vertex> &vertices, const std::vector<uint32_t> &indices)
|
||||
{
|
||||
if (vertices.empty() || indices.size() < 3) return;
|
||||
generate_tangents_range(vertices, indices, 0, indices.size(), 0, vertices.size());
|
||||
}
|
||||
} // namespace geom
|
||||
20
src/scene/tangent_space.h
Normal file
20
src/scene/tangent_space.h
Normal file
@@ -0,0 +1,20 @@
|
||||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
#include <cstddef>
|
||||
#include <core/vk_types.h>
|
||||
|
||||
namespace geom {
|
||||
|
||||
// Generate per-vertex tangents with a robust fallback when MikkTSpace is not available.
|
||||
// - Fills Vertex.tangent (xyz = tangent, w = handedness sign for B = sign * cross(N, T))
|
||||
// - Expects valid normals and UVs; if UVs are degenerate, builds an arbitrary orthonormal basis.
|
||||
void generate_tangents(std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices);
|
||||
|
||||
// Range variant for submeshes (indices [indexStart, indexStart+indexCount), vertices [vertexStart, vertexStart+vertexCount))
|
||||
void generate_tangents_range(std::vector<Vertex>& vertices, const std::vector<uint32_t>& indices,
|
||||
size_t indexStart, size_t indexCount,
|
||||
size_t vertexStart, size_t vertexCount);
|
||||
|
||||
} // namespace geom
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <fastgltf/tools.hpp>
|
||||
#include <fastgltf/util.hpp>
|
||||
#include <optional>
|
||||
#include "tangent_space.h"
|
||||
//> loadimg
|
||||
std::optional<AllocatedImage> load_image(VulkanEngine *engine, fastgltf::Asset &asset, fastgltf::Image &image, bool srgb)
|
||||
{
|
||||
@@ -312,6 +313,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
|
||||
file.materials[mat.name.c_str()] = newMat;
|
||||
|
||||
GLTFMetallic_Roughness::MaterialConstants constants;
|
||||
// Defaults
|
||||
constants.extra[0].x = 1.0f; // normalScale
|
||||
constants.colorFactors.x = mat.pbrData.baseColorFactor[0];
|
||||
constants.colorFactors.y = mat.pbrData.baseColorFactor[1];
|
||||
constants.colorFactors.z = mat.pbrData.baseColorFactor[2];
|
||||
@@ -334,6 +337,8 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
|
||||
materialResources.colorSampler = engine->_samplerManager->defaultLinear();
|
||||
materialResources.metalRoughImage = engine->_whiteImage;
|
||||
materialResources.metalRoughSampler = engine->_samplerManager->defaultLinear();
|
||||
materialResources.normalImage = engine->_flatNormalImage;
|
||||
materialResources.normalSampler = engine->_samplerManager->defaultLinear();
|
||||
|
||||
// set the uniform buffer for the material data
|
||||
materialResources.dataBuffer = file.materialDataBuffer.buffer;
|
||||
@@ -385,6 +390,39 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
|
||||
: engine->_samplerManager->defaultLinear();
|
||||
}
|
||||
}
|
||||
|
||||
// Normal map (tangent-space)
|
||||
if (mat.normalTexture.has_value())
|
||||
{
|
||||
const auto &tex = gltf.textures[mat.normalTexture.value().textureIndex];
|
||||
size_t imgIndex = tex.imageIndex.value();
|
||||
bool hasSampler = tex.samplerIndex.has_value();
|
||||
size_t sampler = hasSampler ? tex.samplerIndex.value() : SIZE_MAX;
|
||||
|
||||
if (imgIndex < gltf.images.size())
|
||||
{
|
||||
auto normalImg = load_image(engine, gltf, gltf.images[imgIndex], false);
|
||||
if (normalImg.has_value())
|
||||
{
|
||||
materialResources.normalImage = *normalImg;
|
||||
std::string key = std::string("normal_") + mat.name.c_str() + "_" + std::to_string(imgIndex);
|
||||
file.images[key] = *normalImg;
|
||||
}
|
||||
else
|
||||
{
|
||||
materialResources.normalImage = images[imgIndex];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
materialResources.normalImage = engine->_flatNormalImage;
|
||||
}
|
||||
materialResources.normalSampler = hasSampler ? file.samplers[sampler]
|
||||
: engine->_samplerManager->defaultLinear();
|
||||
|
||||
// Store normal scale into material constants extra[0].x if available
|
||||
sceneMaterialConstants[data_index].extra[0].x = mat.normalTexture->scale;
|
||||
}
|
||||
// build material
|
||||
newMat->data = engine->metalRoughMaterial.write_material(engine->_deviceManager->device(), passType, materialResources,
|
||||
file.descriptorPool);
|
||||
@@ -442,12 +480,13 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
|
||||
|
||||
fastgltf::iterateAccessorWithIndex<glm::vec3>(gltf, posAccessor,
|
||||
[&](glm::vec3 v, size_t index) {
|
||||
Vertex newvtx;
|
||||
Vertex newvtx{};
|
||||
newvtx.position = v;
|
||||
newvtx.normal = {1, 0, 0};
|
||||
newvtx.color = glm::vec4{1.f};
|
||||
newvtx.uv_x = 0;
|
||||
newvtx.uv_y = 0;
|
||||
newvtx.tangent = glm::vec4(1,0,0,1);
|
||||
vertices[initial_vtx + index] = newvtx;
|
||||
});
|
||||
}
|
||||
@@ -483,6 +522,29 @@ std::optional<std::shared_ptr<LoadedGLTF> > loadGltf(VulkanEngine *engine, std::
|
||||
});
|
||||
}
|
||||
|
||||
// load tangents if present (vec4, w = sign)
|
||||
auto tangents = p.findAttribute("TANGENT");
|
||||
bool hasTangents = tangents != p.attributes.end();
|
||||
if (hasTangents)
|
||||
{
|
||||
fastgltf::iterateAccessorWithIndex<glm::vec4>(gltf, gltf.accessors[(*tangents).second],
|
||||
[&](glm::vec4 v, size_t index) {
|
||||
vertices[initial_vtx + index].tangent = v;
|
||||
});
|
||||
}
|
||||
|
||||
// Generate tangents if missing and we have UVs
|
||||
if (!hasTangents)
|
||||
{
|
||||
size_t primIndexStart = newSurface.startIndex;
|
||||
size_t primIndexCount = newSurface.count;
|
||||
size_t primVertexStart = initial_vtx;
|
||||
size_t primVertexCount = vertices.size() - initial_vtx;
|
||||
geom::generate_tangents_range(vertices, indices,
|
||||
primIndexStart, primIndexCount,
|
||||
primVertexStart, primVertexCount);
|
||||
}
|
||||
|
||||
if (p.materialIndex.has_value())
|
||||
{
|
||||
newSurface.material = materials[p.materialIndex.value()];
|
||||
|
||||
Reference in New Issue
Block a user