Softbody simplification with bones (#876)
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user