Files
2025-05-29 22:31:40 +03:00

561 lines
20 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace Obi
{
[CreateAssetMenu(fileName = "triangle skinmap", menuName = "Obi/Triangle Skinmap", order = 123)]
public class ObiTriangleSkinMap : ScriptableObject
{
private class MasterFace
{
public Vector3 p1;
public Vector3 p2;
public Vector3 p3;
public Vector3 n1;
public Vector3 n2;
public Vector3 n3;
private Vector3 v0;
private Vector3 v1;
private float dot00;
private float dot01;
private float dot11;
public Vector3 faceNormal;
public float size;
public int index;
public uint master;
public void CacheBarycentricData()
{
v0 = p3 - p1;
v1 = p2 - p1;
dot00 = Vector3.Dot(v0, v0);
dot01 = Vector3.Dot(v0, v1);
dot11 = Vector3.Dot(v1, v1);
}
public bool BarycentricCoords(Vector3 point, ref Vector3 coords)
{
// Compute vectors
Vector3 v2 = point - p1;
// Compute dot products
float dot02 = Vector3.Dot(v0, v2);
float dot12 = Vector3.Dot(v1, v2);
// Compute barycentric coordinates
float det = dot00 * dot11 - dot01 * dot01;
if (!Mathf.Approximately(det, 0))
{
float u = (dot11 * dot02 - dot01 * dot12) / det;
float v = (dot00 * dot12 - dot01 * dot02) / det;
coords = new Vector3(1 - u - v, v, u);
return true;
}
return false;
}
}
[Serializable]
public struct SkinTransform
{
public Vector3 position;
public Quaternion rotation;
public Vector3 scale;
public SkinTransform(Vector3 position, Quaternion rotation, Vector3 scale)
{
this.position = position;
this.rotation = rotation;
this.scale = scale;
}
public SkinTransform(Transform transform)
{
position = transform.position;
rotation = transform.rotation;
scale = transform.localScale;
}
public void Apply(Transform transform)
{
transform.position = position;
transform.rotation = rotation;
transform.localScale = scale;
}
public Matrix4x4 GetMatrix4X4()
{
return Matrix4x4.TRS(position, rotation, scale);
}
public void Reset()
{
position = Vector3.zero;
rotation = Quaternion.identity;
scale = Vector3.one;
}
}
[Serializable]
public struct BarycentricPoint
{
public Vector3 barycentricCoords;
public float height;
public static BarycentricPoint zero
{
get
{
return new BarycentricPoint(Vector3.zero, 0);
}
}
public BarycentricPoint(Vector3 position, float height)
{
this.barycentricCoords = position;
this.height = height;
}
}
[Serializable]
public class SlaveVertex
{
public int slaveIndex;
public int masterTriangleIndex;
public BarycentricPoint position;
public BarycentricPoint normal;
public BarycentricPoint tangent;
public static SlaveVertex empty
{
get
{
return new SlaveVertex(-1, -1, BarycentricPoint.zero, BarycentricPoint.zero, BarycentricPoint.zero);
}
}
public bool isEmpty
{
get { return slaveIndex < 0 || masterTriangleIndex < 0; }
}
public SlaveVertex(int slaveIndex, int masterTriangleIndex, BarycentricPoint position, BarycentricPoint normal, BarycentricPoint tangent)
{
this.slaveIndex = slaveIndex;
this.masterTriangleIndex = masterTriangleIndex;
this.position = position;
this.normal = normal;
this.tangent = tangent;
}
}
[HideInInspector] public bool bound = false;
[Range(0, 1)]
[HideInInspector] public float barycentricWeight = 1;
[Range(0, 1)]
[HideInInspector] public float normalAlignmentWeight = 1;
[Range(0, 1)]
[HideInInspector] public float elevationWeight = 1;
// channels:
[HideInInspector] public uint[] m_MasterChannels;
[HideInInspector] public uint[] m_SlaveChannels;
// slave transform:
[HideInInspector] public SkinTransform m_SlaveTransform = new SkinTransform(Vector3.zero, Quaternion.identity, Vector3.one);
// master blueprint and slave mesh:
[HideInInspector] public ObiClothBlueprintBase m_Master;
[HideInInspector] public Mesh m_Slave;
// skinmap data (list of slave mesh vertices)
[HideInInspector] public List<SlaveVertex> skinnedVertices = new List<SlaveVertex>(); /**< skin info for all skinned vertices. */
public ObiClothBlueprintBase master
{
set
{
if (value != m_Master)
{
m_Master = value;
ValidateMasterChannels(true);
}
}
get { return m_Master; }
}
public Mesh slave
{
set
{
if (value != m_Slave)
{
m_Slave = value;
m_SlaveTransform = new SkinTransform(Vector3.zero, Quaternion.identity, Vector3.one);
ValidateSlaveChannels(true);
}
}
get { return m_Slave; }
}
public void OnEnable()
{
ValidateMasterChannels(false);
ValidateSlaveChannels(false);
}
public void Clear()
{
skinnedVertices.Clear();
bound = false;
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
public void ValidateMasterChannels(bool clearChannels)
{
if (m_Master != null)
{
if (m_MasterChannels == null || m_MasterChannels.Length != m_Master.activeParticleCount)
{
Array.Resize(ref m_MasterChannels, m_Master.activeParticleCount);
if (clearChannels)
{
for (int i = 0; i < m_MasterChannels.Length; ++i)
m_MasterChannels[i] = 0x00000001;
}
}
}
else
m_MasterChannels = null;
}
public void ValidateSlaveChannels(bool clearChannels)
{
if (m_Slave != null)
{
if (m_SlaveChannels == null || m_SlaveChannels.Length != m_Slave.vertexCount)
{
Array.Resize(ref m_SlaveChannels, m_Slave.vertexCount);
if (clearChannels)
{
for (int i = 0; i < m_SlaveChannels.Length; ++i)
m_SlaveChannels[i] = 0x00000001;
}
}
}
else
m_SlaveChannels = null;
}
public void CopyChannel(uint[] channels, int source, int dest)
{
int shift = source - dest;
uint destMask = (uint)(1 << dest);
for (int i = 0; i < channels.Length; ++i)
{
// move bit from source to destination:
uint copy = (shift > 0) ? (uint)(channels[i] >> shift) : (uint)(channels[i] << Mathf.Abs(shift));
// clear destination bit and or with displaced source bit:
channels[i] = (channels[i] & ~destMask) | (copy & destMask);
}
}
public void FillChannel(uint[] channels, int channel)
{
for (int i = 0; i < channels.Length; ++i)
channels[i] |= (uint)(1 << channel);
}
public void ClearChannel(uint[] channels, int channel)
{
for (int i = 0; i < channels.Length; ++i)
channels[i] &= ~(uint)(1 << channel);
}
private bool BindToFace(int slaveIndex,
MasterFace triangle,
Vector3 position,
Vector3 normalPoint,
Vector3 tangentPoint,
out SlaveVertex skinning)
{
skinning = SlaveVertex.empty;
BarycentricPoint posBary;
if (FindSkinBarycentricCoords(triangle, position, 24, 0.001f, out posBary))
{
BarycentricPoint normBary;
BarycentricPoint tangentBary;
FindSkinBarycentricCoords(triangle, normalPoint, 16, 0.005f, out normBary);
FindSkinBarycentricCoords(triangle, tangentPoint, 16, 0.005f, out tangentBary);
skinning = new SlaveVertex(slaveIndex, triangle.index, posBary, normBary, tangentBary);
return true;
}
return false;
}
private float GetBarycentricError(Vector3 bary)
{
Vector3 error = bary - Vector3.one * 0.5f;
error[0] = Mathf.Max(Mathf.Abs(error[0]) - 0.5f, 0);
error[1] = Mathf.Max(Mathf.Abs(error[1]) - 0.5f, 0);
error[2] = Mathf.Max(Mathf.Abs(error[2]) - 0.5f, 0);
return error.sqrMagnitude;
}
private float GetFaceMappingError(MasterFace triangle,
SlaveVertex vertex,
Vector3 normal)
{
// initialize the error with barycentric coords error (larger the further away we are from the triangle's limits):
float bary_error = GetBarycentricError(vertex.position.barycentricCoords) * barycentricWeight;
float error = bary_error;
// calculate deviation of point normal from face normal, use it to weight normal barycentric error:
float normal_deviation = Mathf.Clamp01(0.5f * (1.0f - Mathf.Abs(Vector3.Dot(triangle.faceNormal, normal))));
error += normal_deviation * GetBarycentricError(vertex.normal.barycentricCoords) * normalAlignmentWeight;
// make height relative to triangle size, and calculate error weight based on barycentric error:
float height_val = vertex.position.height / Mathf.Max(0.0001f, triangle.size);
float height_w = 0.3f + 2.5f * bary_error;
error += height_w * Mathf.Abs(height_val) * elevationWeight;
return error;
}
/**
* We need to find the barycentric coordinates of point such that the interpolated normal at that point passes trough our target position.
*
* X
* \ / /
* \------/--/
*
* This is necessary to ensure curvature changes in the surface affect skinned points away from the face plane.
* To do so, we use an iterative method similar to Newton´s method for root finding:
*
* - Project the point on the triangle using an initial normal.
* - Get interpolated normal at projection.
* - Intersect line from point and interpolated normal with triangle, to find a new projection.
* - Repeat.
*/
bool FindSkinBarycentricCoords(MasterFace triangle,
Vector3 position,
int max_iterations,
float min_convergence,
out BarycentricPoint barycentricPoint)
{
barycentricPoint = BarycentricPoint.zero;
// start at center of triangle:
Vector3 trusted_bary = Vector3.one / 3.0f;
Vector3 temp_normal;
ObiUtils.BarycentricInterpolation(in triangle.n1,
in triangle.n2,
in triangle.n3,
in trusted_bary,
out temp_normal);
int it = 0;
float trust = 1.0f;
float convergence = float.MaxValue;
while (it++ < max_iterations)
{
Vector3 point;
if (!Obi.ObiUtils.LinePlaneIntersection(triangle.p1, triangle.faceNormal, position, temp_normal, out point))
return false;
// get bary coords at intersection:
Vector3 bary = Vector3.zero;
if (!triangle.BarycentricCoords(point, ref bary))
break;
// calculate error:
Vector3 error = bary - trusted_bary; // distance from current estimation to last trusted estimation.
convergence = Vector3.Dot(error, error); // get a single convergence value.
// weighted sum of bary coords:
trusted_bary = (1.0f - trust) * trusted_bary + trust * bary;
// do we still maintain the barycentric invariant?
if (Mathf.Abs(1.0f - (trusted_bary.x + trusted_bary.y + trusted_bary.z)) > 0.001f)
return false;
// update normal
ObiUtils.BarycentricInterpolation(in triangle.n1,
in triangle.n2,
in triangle.n3,
in trusted_bary,
out temp_normal);
if (convergence < min_convergence)
break;
trust *= 0.8f;
}
Vector3 pos_on_tri = trusted_bary[0] * triangle.p1 +
trusted_bary[1] * triangle.p2 +
trusted_bary[2] * triangle.p3;
float height = Vector3.Dot(position - pos_on_tri, temp_normal);
barycentricPoint.barycentricCoords = trusted_bary;
barycentricPoint.height = height;
return convergence < min_convergence;
}
public IEnumerator Bind()
{
Clear();
if (master == null || slave == null)
yield break;
Vector3[] slavePositions = slave.vertices;
Vector3[] slaveNormals = slave.normals;
Vector4[] slaveTangents = slave.tangents;
Matrix4x4 s2world = m_SlaveTransform.GetMatrix4X4();
Matrix4x4 s2worldNormal = s2world.inverse.transpose;
// count active triangles normals:
int activeDeformableTriangles = 0;
for (int i = 0; i < master.deformableTriangles.Length; i += 3)
{
int t1 = master.deformableTriangles[i];
int t2 = master.deformableTriangles[i + 1];
int t3 = master.deformableTriangles[i + 2];
if (t1 >= master.activeParticleCount || t2 >= master.activeParticleCount || t3 >= master.activeParticleCount)
continue;
activeDeformableTriangles++;
}
// generate master triangle info:
MasterFace[] masterFaces = new MasterFace[activeDeformableTriangles];
int count = 0;
for (int i = 0; i < master.deformableTriangles.Length; i += 3)
{
MasterFace face = new MasterFace();
int t1 = master.deformableTriangles[i];
int t2 = master.deformableTriangles[i + 1];
int t3 = master.deformableTriangles[i + 2];
if (t1 >= master.activeParticleCount || t2 >= master.activeParticleCount || t3 >= master.activeParticleCount)
continue;
face.p1 = master.positions[t1];
face.p2 = master.positions[t2];
face.p3 = master.positions[t3];
face.n1 = master.restNormals[t1];
face.n2 = master.restNormals[t2];
face.n3 = master.restNormals[t3];
face.master = m_MasterChannels[t1] |
m_MasterChannels[t2] |
m_MasterChannels[t3];
face.size = ((face.p1 - face.p2).magnitude +
(face.p1 - face.p3).magnitude +
(face.p2 - face.p3).magnitude) / 3.0f;
face.faceNormal = Vector3.Cross(face.p2 - face.p1, face.p3 - face.p1).normalized;
face.index = i;
face.CacheBarycentricData();
masterFaces[count++] = face;
if (i % 10 == 0)
yield return new CoroutineJob.ProgressInfo("Generating master faces...", count / (float)masterFaces.Length);
}
// for each slave vertex, find the best fitting master triangle:
for (int i = 0; i < slavePositions.Length; ++i)
{
// if vertex slave channel is deactivated, don´t skin it.
if (m_SlaveChannels[i] == 0) continue;
// initialize best triangle error and index:
float bestError = float.MaxValue;
SlaveVertex bestSkinning = SlaveVertex.empty;
Vector3 worldPos = s2world.MultiplyPoint3x4(slavePositions[i]);
Vector3 worldNormalDir = s2worldNormal.MultiplyVector(slaveNormals[i]).normalized;
Vector3 worldNormalPoint = worldPos + worldNormalDir * 0.05f;
Vector3 worldTangentPoint = worldPos + s2worldNormal.MultiplyVector(new Vector3(slaveTangents[i].x, slaveTangents[i].y, slaveTangents[i].z).normalized) * 0.05f;
// find the best fitting master face:
for (int j = 0; j < masterFaces.Length; ++j)
{
MasterFace face = masterFaces[j];
// if the triangle master channel and the target slave channel do not overlap, skip it.
if ((face.master & m_SlaveChannels[i]) == 0)
continue;
// calculate skinning data for this face:
SlaveVertex skinning;
if (BindToFace(i, face, worldPos, worldNormalPoint, worldTangentPoint, out skinning))
{
// calculate mapping error for this triangle:
float error = GetFaceMappingError(face, skinning, worldNormalDir);
// if the error is less than the best, update it.
if (error < bestError)
{
bestError = error;
bestSkinning = skinning;
}
}
}
// skin target vertex to the best source triangle found, if any:
if (!bestSkinning.isEmpty)
skinnedVertices.Add(bestSkinning);
if (i % 5 == 0)
yield return new CoroutineJob.ProgressInfo("Skinning slave vertices (" + i + "/" + slavePositions.Length + ")...", i / (float)slavePositions.Length);
}
bound = true;
#if UNITY_EDITOR
EditorUtility.SetDirty(this);
#endif
}
}
}