Softbody simplification with bones (#876)

This commit is contained in:
Turánszki János
2024-07-04 09:24:47 +02:00
committed by GitHub
parent faff256d18
commit fb41bf83bd
55 changed files with 1167 additions and 778 deletions
+261 -53
View File
@@ -13,6 +13,7 @@
#include "wiLua.h"
#include "Utility/mikktspace.h"
#include "Utility/meshoptimizer/meshoptimizer.h"
#if __has_include("OpenImageDenoise/oidn.hpp")
#include "OpenImageDenoise/oidn.hpp"
@@ -832,7 +833,8 @@ namespace wi::scene
AlignTo(uv_count * sizeof(Vertex_UVS), alignment) +
AlignTo(vertex_atlas.size() * sizeof(Vertex_TEX), alignment) +
AlignTo(vertex_colors.size() * sizeof(Vertex_COL), alignment) +
AlignTo(vertex_boneindices.size() * sizeof(Vertex_BON), alignment)
AlignTo(vertex_boneindices.size() * sizeof(Vertex_BON), alignment) +
AlignTo(vertex_boneindices2.size() * sizeof(Vertex_BON), alignment)
;
constexpr Format morph_format = Format::R16G16B16A16_FLOAT;
@@ -1025,29 +1027,69 @@ namespace wi::scene
}
}
// skinning buffers:
// bone reference buffers (skinning, soft body):
if (!vertex_boneindices.empty())
{
vb_bon.offset = buffer_offset;
vb_bon.size = vertex_boneindices.size() * sizeof(Vertex_BON);
const size_t influence_div4 = GetBoneInfluenceDiv4();
vb_bon.size = (vertex_boneindices.size() + vertex_boneindices2.size()) * sizeof(Vertex_BON);
Vertex_BON* vertices = (Vertex_BON*)(buffer_data + buffer_offset);
buffer_offset += AlignTo(vb_bon.size, alignment);
assert(vertex_boneindices.size() == vertex_boneweights.size());
assert(vertex_boneindices.size() == vertex_boneweights.size()); // must have same number of indices as weights
assert(vertex_boneindices2.empty() || vertex_boneindices2.size() == vertex_boneindices.size()); // if second influence stream exists, it must be as large as the first
assert(vertex_boneindices2.size() == vertex_boneweights2.size()); // must have same number of indices as weights
for (size_t i = 0; i < vertex_boneindices.size(); ++i)
{
XMFLOAT4& wei = vertex_boneweights[i];
// normalize bone weights
float len = wei.x + wei.y + wei.z + wei.w;
if (len > 0)
// Normalize weights:
// Note: if multiple influence streams are present,
// we have to normalize them together, not separately
float weights[8] = {};
weights[0] = vertex_boneweights[i].x;
weights[1] = vertex_boneweights[i].y;
weights[2] = vertex_boneweights[i].z;
weights[3] = vertex_boneweights[i].w;
if (influence_div4 > 1)
{
wei.x /= len;
wei.y /= len;
wei.z /= len;
wei.w /= len;
weights[4] = vertex_boneweights2[i].x;
weights[5] = vertex_boneweights2[i].y;
weights[6] = vertex_boneweights2[i].z;
weights[7] = vertex_boneweights2[i].w;
}
float sum = 0;
for (auto& weight : weights)
{
sum += weight;
}
if (sum > 0)
{
const float norm = 1.0f / sum;
for (auto& weight : weights)
{
weight *= norm;
}
}
// Store back normalized weights:
vertex_boneweights[i].x = weights[0];
vertex_boneweights[i].y = weights[1];
vertex_boneweights[i].z = weights[2];
vertex_boneweights[i].w = weights[3];
if (influence_div4 > 1)
{
vertex_boneweights2[i].x = weights[4];
vertex_boneweights2[i].y = weights[5];
vertex_boneweights2[i].z = weights[6];
vertex_boneweights2[i].w = weights[7];
}
Vertex_BON vert;
vert.FromFULL(vertex_boneindices[i], wei);
std::memcpy(vertices + i, &vert, sizeof(vert));
vert.FromFULL(vertex_boneindices[i], vertex_boneweights[i]);
std::memcpy(vertices + (i * influence_div4 + 0), &vert, sizeof(vert));
if (influence_div4 > 1)
{
vert.FromFULL(vertex_boneindices2[i], vertex_boneweights2[i]);
std::memcpy(vertices + (i * influence_div4 + 1), &vert, sizeof(vert));
}
}
}
@@ -1980,54 +2022,220 @@ namespace wi::scene
return PathDataType::Event;
}
void SoftBodyPhysicsComponent::CreateFromMesh(const MeshComponent& mesh)
void SoftBodyPhysicsComponent::CreateFromMesh(MeshComponent& mesh)
{
vertex_positions_simulation.resize(mesh.vertex_positions.size());
vertex_normals_simulation.resize(mesh.vertex_normals.size());
vertex_tangents_simulation.resize(mesh.vertex_tangents.size());
XMFLOAT3 _min = XMFLOAT3(std::numeric_limits<float>::max(), std::numeric_limits<float>::max(), std::numeric_limits<float>::max());
XMFLOAT3 _max = XMFLOAT3(std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest(), std::numeric_limits<float>::lowest());
XMMATRIX W = XMLoadFloat4x4(&worldMatrix);
for (size_t i = 0; i < mesh.vertex_positions.size(); ++i)
if (weights.size() != mesh.vertex_positions.size())
{
XMFLOAT3 pos = mesh.vertex_positions[i];
XMStoreFloat3(&pos, XMVector3Transform(XMLoadFloat3(&pos), W));
vertex_positions_simulation[i].FromFULL(pos);
_min = wi::math::Min(_min, pos);
_max = wi::math::Max(_max, pos);
weights.resize(mesh.vertex_positions.size());
std::fill(weights.begin(), weights.end(), 1.0f);
}
aabb = AABB(_min, _max);
if (physicsToGraphicsVertexMapping.empty())
if (physicsIndices.empty())
{
// Create a mapping that maps unique vertex positions to all vertex indices that share that. Unique vertex positions will make up the physics mesh:
wi::unordered_map<size_t, uint32_t> uniquePositions;
graphicsToPhysicsVertexMapping.resize(mesh.vertex_positions.size());
physicsToGraphicsVertexMapping.clear();
weights.clear();
for (size_t i = 0; i < mesh.vertex_positions.size(); ++i)
bool pinning_required = false;
wi::vector<uint32_t> source;
uint32_t first_subset = 0;
uint32_t last_subset = 0;
mesh.GetLODSubsetRange(0, first_subset, last_subset);
for (uint32_t subsetIndex = first_subset; subsetIndex < last_subset; ++subsetIndex)
{
const XMFLOAT3& position = mesh.vertex_positions[i];
size_t hashes[] = {
std::hash<int>{}(int(position.x * detail)),
std::hash<int>{}(int(position.y * detail)),
std::hash<int>{}(int(position.z * detail)),
};
size_t vertexHash = (((hashes[0] ^ (hashes[1] << 1) >> 1) ^ (hashes[2] << 1)) >> 1);
if (uniquePositions.count(vertexHash) == 0)
const MeshComponent::MeshSubset& subset = mesh.subsets[subsetIndex];
const uint32_t* indices = mesh.indices.data() + subset.indexOffset;
for (uint32_t i = 0; i < subset.indexCount; ++i)
{
uniquePositions[vertexHash] = (uint32_t)physicsToGraphicsVertexMapping.size();
physicsToGraphicsVertexMapping.push_back((uint32_t)i);
source.push_back(indices[i]);
pinning_required |= weights[indices[i]] == 0;
}
graphicsToPhysicsVertexMapping[i] = uniquePositions[vertexHash];
}
physicsIndices.resize(source.size());
if (pinning_required)
{
// If there is pinning, we need to use precise LOD to retain difference between pinned and soft vertices:
wi::vector<XMFLOAT4> vertices(mesh.vertex_positions.size());
for (size_t i = 0; i < mesh.vertex_positions.size(); ++i)
{
vertices[i].x = mesh.vertex_positions[i].x;
vertices[i].y = mesh.vertex_positions[i].y;
vertices[i].z = mesh.vertex_positions[i].z;
vertices[i].w = weights[i] == 0 ? 1.0f : 0.0f;
}
// Generate shadow indices for position+weight-only stream:
wi::vector<uint32_t> shadow_indices(source.size());
meshopt_generateShadowIndexBuffer(
shadow_indices.data(), source.data(), source.size(),
vertices.data(), vertices.size(), sizeof(XMFLOAT4), sizeof(XMFLOAT4)
);
size_t result = 0;
size_t target_index_count = size_t(shadow_indices.size() * saturate(detail)) / 3 * 3;
float target_error = 1 - saturate(detail);
int tries = 0;
while (result == 0 && tries < 100)
{
result = meshopt_simplify(
&physicsIndices[0],
&shadow_indices[0],
shadow_indices.size(),
(const float*)&mesh.vertex_positions[0],
mesh.vertex_positions.size(),
sizeof(XMFLOAT3),
target_index_count,
target_error
);
target_error *= 0.5f;
}
assert(result > 0);
physicsIndices.resize(result);
}
else
{
// Sloppy LOD can be used if no pinning is required:
size_t result = 0;
size_t target_index_count = 0;
float target_error = sqr(1 - saturate(detail));
int tries = 0;
while (result == 0 && tries < 100)
{
result = meshopt_simplifySloppy(
&physicsIndices[0],
&source[0],
source.size(),
(const float*)&mesh.vertex_positions[0],
mesh.vertex_positions.size(),
sizeof(XMFLOAT3),
target_index_count,
target_error
);
target_error *= 0.5f;
}
assert(result > 0);
physicsIndices.resize(result);
}
weights.resize(physicsToGraphicsVertexMapping.size());
std::fill(weights.begin(), weights.end(), 1.0f);
physicsIndices.shrink_to_fit();
// Remap physics indices to point to physics indices:
physicsToGraphicsVertexMapping.clear();
wi::unordered_map<uint32_t, size_t> physicsVertices;
for (size_t i = 0; i < physicsIndices.size(); ++i)
{
const uint32_t graphicsInd = physicsIndices[i];
if (physicsVertices.count(graphicsInd) == 0)
{
physicsVertices[graphicsInd] = physicsToGraphicsVertexMapping.size();
physicsToGraphicsVertexMapping.push_back(graphicsInd);
}
physicsIndices[i] = (uint32_t)physicsVertices[graphicsInd];
}
physicsToGraphicsVertexMapping.shrink_to_fit();
// BoneQueue is used for assigning the highest weighted fixed number of bones (soft body nodes) to a graphics vertex
static constexpr int influence = 8;
struct BoneQueue
{
struct Bone
{
uint32_t index = 0;
float weight = 0;
constexpr bool operator<(const Bone& other) const { return weight < other.weight; }
constexpr bool operator>(const Bone& other) const { return weight > other.weight; }
};
Bone bones[influence];
constexpr void add(uint32_t index, float weight)
{
int mini = 0;
for (int i = 1; i < arraysize(bones); ++i)
{
if (bones[i].weight < bones[mini].weight)
{
mini = i;
}
}
if (weight > bones[mini].weight)
{
bones[mini].weight = weight;
bones[mini].index = index;
}
}
void finalize()
{
std::sort(bones, bones + arraysize(bones), std::greater<Bone>());
// Note: normalization of bone weights will be done in MeshComponent::CreateRenderData()
}
constexpr XMUINT4 get_indices() const
{
return XMUINT4(
influence < 1 ? 0 : bones[0].index,
influence < 2 ? 0 : bones[1].index,
influence < 3 ? 0 : bones[2].index,
influence < 4 ? 0 : bones[3].index
);
}
constexpr XMUINT4 get_indices2() const
{
return XMUINT4(
influence < 5 ? 0 : bones[4].index,
influence < 6 ? 0 : bones[5].index,
influence < 7 ? 0 : bones[6].index,
influence < 8 ? 0 : bones[7].index
);
}
constexpr XMFLOAT4 get_weights() const
{
return XMFLOAT4(
influence < 1 ? 0 : bones[0].weight,
influence < 2 ? 0 : bones[1].weight,
influence < 3 ? 0 : bones[2].weight,
influence < 4 ? 0 : bones[3].weight
);
}
constexpr XMFLOAT4 get_weights2() const
{
return XMFLOAT4(
influence < 5 ? 0 : bones[4].weight,
influence < 6 ? 0 : bones[5].weight,
influence < 7 ? 0 : bones[6].weight,
influence < 8 ? 0 : bones[7].weight
);
}
};
// Create skinning bone vertex data:
mesh.vertex_boneindices.resize(mesh.vertex_positions.size());
mesh.vertex_boneweights.resize(mesh.vertex_positions.size());
if (influence > 4)
{
mesh.vertex_boneindices2.resize(mesh.vertex_positions.size());
mesh.vertex_boneweights2.resize(mesh.vertex_positions.size());
}
wi::jobsystem::context ctx;
wi::jobsystem::Dispatch(ctx, (uint32_t)mesh.vertex_positions.size(), 64, [&](wi::jobsystem::JobArgs args) {
const XMFLOAT3 position = mesh.vertex_positions[args.jobIndex];
BoneQueue bones;
for (size_t physicsInd = 0; physicsInd < physicsToGraphicsVertexMapping.size(); ++physicsInd)
{
const uint32_t graphicsInd = physicsToGraphicsVertexMapping[physicsInd];
const XMFLOAT3 position2 = mesh.vertex_positions[graphicsInd];
const float dist = wi::math::DistanceSquared(position, position2);
// Note: 0.01 correction is carefully tweaked so that cloth_test and sponza curtains look good
// (larger values blow up the curtains, lower values make the shading of the cloth look bad)
const float weight = 1.0f / (0.01f + dist);
bones.add((uint32_t)physicsInd, weight);
}
bones.finalize();
mesh.vertex_boneindices[args.jobIndex] = bones.get_indices();
mesh.vertex_boneweights[args.jobIndex] = bones.get_weights();
if (influence > 4)
{
mesh.vertex_boneindices2[args.jobIndex] = bones.get_indices2();
mesh.vertex_boneweights2[args.jobIndex] = bones.get_weights2();
}
});
wi::jobsystem::Wait(ctx);
mesh.CreateRenderData();
}
}