init cloth dev

This commit is contained in:
ousttrue
2024-10-17 19:01:07 +09:00
parent bab108570e
commit bbcb74c4ed
65 changed files with 5193 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 6871c022e581b3742bbc892f38900607
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a00a3e7f5bad4ae4cbbf0e2acc739fa8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
{
"name": "RotateParticle",
"rootNamespace": "",
"references": [
"GUID:0138a2c7639266f45b948ea162125de5"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 308b348fb80d89d42a9620951b0f60db
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2ba7e498c4d5eef44aba4cf125686089
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,13 @@
namespace RotateParticle
{
public struct FrameTime
{
public readonly float DeltaTime;
public readonly float SqDt;
public FrameTime(float deltaTime)
{
DeltaTime = deltaTime;
SqDt = deltaTime * deltaTime;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 568e5702a44c18d4487775441e011a61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,233 @@
using System.Collections.Generic;
using System.Linq;
using SphereTriangle;
using UnityEngine;
namespace RotateParticle
{
public class HumanoidAutoSetup : MonoBehaviour
{
static (string group, HumanBodyBones head, HumanBodyBones tail, float radius)[] Capsules = new[]
{
("Leg", HumanBodyBones.LeftUpperLeg, HumanBodyBones.LeftLowerLeg, 0.06f),
("Leg", HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot, 0.05f),
("Leg", HumanBodyBones.RightUpperLeg, HumanBodyBones.RightLowerLeg, 0.06f),
("Leg", HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot, 0.05f),
("Arm", HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, 0.03f),
("Arm", HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand, 0.03f),
("Arm", HumanBodyBones.LeftHand, HumanBodyBones.LeftMiddleProximal, 0.02f),
("Arm", HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, 0.03f),
("Arm", HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand, 0.03f),
("Arm", HumanBodyBones.RightHand, HumanBodyBones.RightMiddleProximal, 0.02f),
};
T GetOrAddComponent<T>() where T : Component
{
var t = GetComponent<T>();
if (t != null)
{
return t;
}
return gameObject.AddComponent<T>();
}
bool TryAddGroup(CollisionGroupMask mask, string name, Animator animator, HumanBodyBones humanBone, string[] targets, StrandConnectionType type,
out StrandGroup group)
{
var bone = animator.GetBoneTransform(humanBone);
if (bone == null)
{
Debug.LogWarning($"{humanBone} not found");
group = default;
return false;
}
List<Transform> transforms = new();
foreach (Transform child in bone)
{
foreach (var target in targets)
{
if (child.name.ToLower().Contains(target.ToLower()))
{
transforms.Add(child);
break;
}
}
}
if (transforms.Count == 0)
{
Debug.LogWarning($"{string.Join(',', targets)} not found");
group = default;
return false;
}
group = new StrandGroup
{
Name = name,
CollisionMask = mask,
DefaultStrandRaius = 0.02f,
Connection = type
};
group.Roots.AddRange(transforms);
// sort
group.Roots.Sort(new TransformSort(bone.position));
return true;
}
bool TryAddGroupChildChild(CollisionGroupMask mask, string name, Animator animator, HumanBodyBones humanBone,
string[] targets, string[] excludes,
StrandConnectionType type,
bool sort,
out StrandGroup group)
{
var bone = animator.GetBoneTransform(humanBone);
if (bone == null)
{
Debug.LogWarning($"{humanBone} not found");
group = default;
return false;
}
List<Transform> transforms = new();
foreach (Transform child in bone)
{
foreach (Transform childchild in child)
{
if (excludes.Any(x => childchild.name.ToLower().Contains(x.ToLower())))
{
continue;
}
foreach (var target in targets)
{
if (childchild.name.ToLower().Contains(target.ToLower()))
{
transforms.Add(childchild);
break;
}
}
}
}
if (transforms.Count == 0)
{
Debug.LogWarning($"{string.Join(',', targets)} not found");
group = default;
return false;
}
group = new StrandGroup
{
Name = name,
CollisionMask = mask,
DefaultStrandRaius = 0.02f,
Connection = type
};
group.Roots.AddRange(transforms);
// sort
if (sort)
{
group.Roots.Sort(new TransformSort(bone.position));
}
return true;
}
public void Reset()
{
var animator = GetComponent<Animator>();
if (animator == null)
{
Debug.LogWarning("no animator");
return;
}
var avatar = animator.avatar;
if (!avatar.isHuman)
{
Debug.LogWarning("not humanoid");
return;
}
var system = GetOrAddComponent<RotateParticleSystem>();
system.Env.DragForce = 0.6f;
system.Env.Stiffness = 0.07f;
foreach (var (group, head, tail, radius) in Capsules)
{
system.AddColliderIfNotExists(group,
animator.GetBoneTransform(head), animator.GetBoneTransform(tail), radius);
}
// skirt
{
if (TryAddGroup(CollisionGroupMask.Group01, "Skirt", animator, HumanBodyBones.Hips, new[] { "skirt", "スカート", "スカート" }, StrandConnectionType.ClothLoop,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroupChildChild(CollisionGroupMask.Group01, "Skirt", animator, HumanBodyBones.Hips, new[] { "skirt", "スカート", "スカート" }, new string[]{}, StrandConnectionType.ClothLoop, false,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroup(CollisionGroupMask.Group02, "髪", animator, HumanBodyBones.Head, new[] { "髪", "hair" }, StrandConnectionType.Strand,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroup(CollisionGroupMask.Group01, "裾", animator, HumanBodyBones.Hips, new[] { "裾" }, StrandConnectionType.Cloth,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroupChildChild(CollisionGroupMask.Group02, "左袖", animator, HumanBodyBones.LeftUpperArm, new[] { "袖" }, new[] { "ひじ袖" }, StrandConnectionType.ClothLoop,
false,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroupChildChild(CollisionGroupMask.Group02, "左ひじ袖", animator, HumanBodyBones.LeftLowerArm, new[] { "袖" }, new string[] { }, StrandConnectionType.ClothLoop,
false,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroupChildChild(CollisionGroupMask.Group02, "右袖", animator, HumanBodyBones.RightUpperArm, new[] { "袖" }, new[] { "ひじ袖" }, StrandConnectionType.ClothLoop,
false,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroupChildChild(CollisionGroupMask.Group02, "右ひじ袖", animator, HumanBodyBones.RightLowerArm, new[] { "袖" }, new string[] { }, StrandConnectionType.ClothLoop,
false,
out var g))
{
system._strandGroups.Add(g);
}
}
{
if (TryAddGroup(CollisionGroupMask.Group01 | CollisionGroupMask.Group02, "マント", animator, HumanBodyBones.Chest, new[] { "マント" }, StrandConnectionType.Cloth,
out var g))
{
system._strandGroups.Add(g);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f2c23a62e016b3244b286404bc339540
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace RotateParticle
{
public class HumanoidPose
{
public Vector3 HipsPosition;
public List<(HumanBodyBones, Quaternion)> Rotations = new();
public HumanoidPose(Animator aninmator)
{
foreach (HumanBodyBones bone in Enum.GetValues(typeof(HumanBodyBones)))
{
if (bone == HumanBodyBones.LastBone)
{
continue;
}
var t = aninmator.GetBoneTransform(bone);
if (t != null)
{
if (bone == HumanBodyBones.Hips)
{
HipsPosition = t.localPosition;
}
Rotations.Add((bone, t.localRotation));
}
}
}
public static void ApplyLerp(Animator a, HumanoidPose start, HumanoidPose end, float t)
{
foreach (var (b, sr, er) in Enumerable.Zip(start.Rotations, end.Rotations, (s, e) =>
{
var (sb, sr) = s;
var (eb, er) = e;
if (sb != eb)
{
throw new Exception();
}
return (sb, sr, er);
}))
{
var transform = a.GetBoneTransform(b);
if (transform == null)
{
throw new Exception();
}
transform.localRotation = Quaternion.Slerp(sr, er, t);
if (b == HumanBodyBones.Hips)
{
transform.localPosition = Vector3.Lerp(start.HipsPosition, end.HipsPosition, t);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 500c97fb71417394e8144b008b462e6c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,49 @@
using System;
using SphereTriangle;
using UnityEngine;
namespace RotateParticle
{
[Serializable]
public struct ParticleInitState
{
public readonly int Index;
[SerializeField]
public Vector3 LocalPosition;
[SerializeField]
public Quaternion LocalRotation;
[SerializeField]
public Vector3 BoneAxis;
[SerializeField]
public float StrandLength;
[SerializeField]
public float Radius;
// 0 は移動しない固定(回転はしてもよい)
// TODO: force / mass => accelaration
public float Mass;
public CollisionGroupMask CollisionMask;
public ParticleInitState(int index, Transform t, float radius, float mass, CollisionGroupMask collisionMask)
{
Index = index;
LocalPosition = t.localPosition;
LocalRotation = t.localRotation;
StrandLength = LocalPosition.magnitude;
BoneAxis = LocalPosition.normalized;
Radius = radius;
Mass = mass;
CollisionMask = collisionMask;
}
public override string ToString()
{
return $"create Particle: {LocalPosition}, {BoneAxis}, {StrandLength}";
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4bccf867d0bc7b244841c8ecff9ce65a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
using Unity.Collections;
using Unity.Jobs;
using UnityEngine;
namespace RotateParticle
{
public struct ParticleJob : IJob
{
public float DeltaTime;
public float Stiffness;
public float DragForce;
public Vector3 External;
public int Index;
public int ParentIndex;
public float Mass;
public Vector3 Rest;
public Vector3 CurrentPosition;
public Vector3 Prev;
public Quaternion ParentParentRotation;
public Quaternion InitParentLocalRotation;
public Vector3 InitBoneAxis;
public Vector3 ParentPosition;
public NativeArray<Vector3> NewPos;
public NativeArray<Quaternion> NewRot;
public void Execute()
{
var tr = Step();
if (tr.HasValue)
{
var (newPos, parentRot) = tr.Value;
NewPos[Index] = newPos;
NewRot[ParentIndex] = parentRot;
}
}
public (Vector3 NewPos, Quaternion ParentRotation)? Step()
{
if (Mass == 0)
{
return default;
}
var sqDt = DeltaTime * DeltaTime;
var f = (Rest - CurrentPosition) * Stiffness / sqDt;
// Debug.Log($"{rest}");
f += (External / sqDt);
var newPos = CurrentPosition + (CurrentPosition - Prev) * (1 - DragForce) + f * sqDt;
var parentRotation = CalcParentRotation(newPos, ParentParentRotation, InitParentLocalRotation, InitBoneAxis, ParentPosition);
return (newPos, parentRotation);
}
public static Quaternion CalcParentRotation(in Vector3 newPos,
in Quaternion parentparent,
in Quaternion initParentLocalRotation,
in Vector3 initBoneAxis,
in Vector3 parentPosition)
{
// 親の回転として結果を適用する(位置から回転を作る)
var restRotation = parentparent * initParentLocalRotation;
var r = CalcRotation(restRotation, initBoneAxis, newPos - parentPosition);
// _runtime = new ParticleRuntimeState(_runtime.CurrentPosition, transform.position);
return r;
}
static Quaternion CalcRotation(Quaternion restRotation, Vector3 boneAxis, Vector3 to)
{
Quaternion aimRotation = Quaternion.FromToRotation(restRotation * boneAxis, to);
return aimRotation * restRotation;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7127f077eba307343a5f5ce15213a595
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using SphereTriangle;
using UnityEngine;
namespace RotateParticle
{
[Serializable]
public class ParticleList
{
public List<RotateParticle> _particles = new();
public List<Transform> _particleTransforms = new();
public Strand MakeParticleStrand(SimulationEnv env, Transform t, float radius, CollisionGroupMask mask)
{
var strand = new Strand(mask);
_MakeParticlesRecursive(strand, env, 0, t, radius, null);
return strand;
}
RotateParticle _MakeParticlesRecursive(Strand strand, SimulationEnv env, int child_index, Transform t, float radius, RotateParticle parent)
{
var mass = parent == null ? 0 : 1;
if (child_index > 0)
{
// 枝分かれ動かない
mass = 0;
}
var particle_index = _MakeAParticle(env, t, radius, parent, mass, strand.CollisionMask);
var joint = _particles[particle_index];
strand.Particles.Add(joint);
for (int i = 0; i < t.childCount; ++i)
{
var child = t.GetChild(i);
var child_joint = _MakeParticlesRecursive(strand, env, i, child, radius, joint);
joint.Children.Add(child_joint);
}
return joint;
}
int _MakeAParticle(SimulationEnv env, Transform t, float radius, RotateParticle parent, float mass, CollisionGroupMask collisionMask)
{
var index = _particles.Count;
_particleTransforms.Add(t);
_particles.Add(new RotateParticle(index, parent, env, t, radius, mass, collisionMask));
return index;
}
public void EndInitialize(SphereTriangle.InitPosition initPos)
{
for (int i = 0; i < _particles.Count; ++i)
{
initPos(i, _particles[i].Init.Mass, _particleTransforms[i].position);
}
}
public void BeginFrame(SimulationEnv env, FrameTime time, IReadOnlyList<Vector3> restPositions)
{
for (int i = 0; i < _particles.Count; ++i)
{
var p = _particles[i];
if (p.Init.Mass <= 0)
{
continue;
}
var rest = restPositions[p.Init.Index];
p.BeginFrame(env, time, rest);
}
}
public void Verlet(SimulationEnv env, FrameTime time, SphereTriangle.InitPosition initPos)
{
for (int i = 0; i < _particles.Count; ++i)
{
var p = _particles[i];
if (p.Init.Mass > 0)
{
var newPos = p.Verlet(env, time);
initPos(i, p.Init.Mass, newPos);
}
}
}
public void DrawGizmos()
{
foreach (var p in _particles)
{
p.OnDrawGizmos(_particleTransforms[p.Init.Index]);
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fee618dfb5846ea47978b49ed4c0282b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,42 @@
using System;
using UnityEngine;
namespace RotateParticle
{
public class ParticleRuntimeState
{
public Vector3 Current;
public Vector3 Prev;
public ParticleRuntimeState(SimulationEnv env, Transform transform)
{
if (env.Center == null)
{
Prev = transform.position;
Current = transform.position;
}
else
{
throw new NotImplementedException();
}
}
public void Update(Transform t)
{
Current = t.position;
}
public void Apply(in Vector3 newPos, bool zeroVelocity = false)
{
if (zeroVelocity)
{
Prev = newPos;
}
else
{
Prev = Current;
}
Current = newPos;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: eddc8c1b804734c49a6eb04d72afb0a8
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,22 @@
using UnityEngine;
namespace RotateParticle
{
public struct RigidTransform
{
public readonly Vector3 Position;
public readonly Quaternion Rotation;
public RigidTransform(in Vector3 pos, in Quaternion rot)
{
Position = pos;
Rotation = rot;
}
public RigidTransform(Transform t)
{
Position = t.position;
Rotation = t.rotation;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1fe036d9e9352e54ea009e8322a4faa2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using SphereTriangle;
using UnityEngine;
namespace RotateParticle
{
[Serializable]
public class RotateParticle
{
public ParticleInitState Init;
public ParticleRuntimeState State;
public readonly List<RotateParticle> Children = new();
public readonly RotateParticle Parent;
// 現フレームの力積算
public Vector3 Force = Vector3.zero;
// 直前で接触があった
public bool HasCollide = false;
public RotateParticle(int index, RotateParticle parent, SimulationEnv env, Transform transform, float radius, float mass, CollisionGroupMask collisionMask)
{
Init = new ParticleInitState(index, transform, radius, mass, collisionMask);
State = new ParticleRuntimeState(env, transform);
Parent = parent;
}
public void BeginFrame(SimulationEnv env, FrameTime time, in Vector3 rest)
{
// integrate forces
Force = Vector3.zero;
// 曲げ
if (HasCollide)
{
// 震え防止。ちょっとマイルドになるような気もする?
}
else
{
// Stiffness: 1 で即時に元に戻る
Force += (rest - State.Current) * env.Stiffness / time.SqDt;
}
// 外力(sqDtで割るとピーキーすぎるのでこれでいいのでは)
Force += env.External / time.DeltaTime;
}
public Vector3 Verlet(SimulationEnv env, FrameTime time)
{
var velocity = (State.Current - State.Prev);
if (HasCollide)
{
// 震え防止。ちょっとマイルドになるような気もする?
velocity = Vector3.zero;
}
else
{
// DragForce: 1 で即時停止
velocity *= (1 - env.DragForce);
}
HasCollide = false;
return State.Current + velocity + Force * time.SqDt;
}
/// <summary>
/// get ParentParent.rotatio * Parent.Init.LocalRotation
/// </summary>
/// <param name="transforms"></param>
/// <returns></returns>
public Quaternion RestRotation(IReadOnlyList<Transform> transforms)
{
if (Parent == null)
{
return Quaternion.identity;
}
var parent = transforms[Parent.Init.Index];
if (Parent.Parent == null)
{
var pt = parent.parent;
if (pt == null)
{
return Parent.Init.LocalRotation;
}
return pt.rotation * Parent.Init.LocalRotation;
}
var parentparent = transforms[Parent.Parent.Init.Index];
var restRotation = parentparent.rotation * Parent.Init.LocalRotation;
return restRotation;
}
public void OnDrawGizmos(Transform transform)
{
if (Init.Radius == 0)
{
return;
}
Gizmos.color = Init.Mass == 0 ? Color.red : Color.gray;
if (transform.parent != null && Init.Mass > 0)
{
Gizmos.matrix = Matrix4x4.identity;
Gizmos.DrawLine(transform.parent.position, transform.position);
}
Gizmos.matrix = transform.localToWorldMatrix;
Gizmos.DrawWireSphere(Vector3.zero, Init.Radius);
var r = Init.Radius * 2;
Gizmos.color = Color.red;
Gizmos.DrawLine(Vector3.left * r, Vector3.right * r);
Gizmos.color = Color.green;
Gizmos.DrawLine(Vector3.down * r, Vector3.up * r);
Gizmos.color = Color.blue;
Gizmos.DrawLine(Vector3.back * r, Vector3.forward * r);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 125347015f2366e4b8957431407ddf9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,363 @@
using System;
using System.Collections.Generic;
using System.Linq;
using SphereTriangle;
using UnityEngine;
namespace RotateParticle
{
[DisallowMultipleComponent]
public class RotateParticleSystem : MonoBehaviour
{
// TODO: param to _strandGroups
[SerializeField]
public SimulationEnv Env = new();
[SerializeField]
public List<StrandGroup> _strandGroups = new List<StrandGroup>();
[SerializeField]
public List<ColliderGroup> _colliderGroups = new();
[Range(0, 1)]
public float _clothFactor = 0.5f;
// runtime
bool _initialized = false;
List<Strand> _strands = new List<Strand>();
public ParticleList _list = new();
public List<(SpringConstraint, ClothRectCollision)> _clothRects = new();
public PositionList _newPos;
Vector3[] _restPositions;
static Color[] Colors = new Color[]
{
Color.yellow,
Color.green,
Color.magenta,
};
Color GetGizmoColor(ColliderGroup g)
{
for (int i = 0; i < _colliderGroups.Count; ++i)
{
if (_colliderGroups[i] == g)
{
return Colors[i];
}
}
return Color.gray;
}
public void AddColliderIfNotExists(string groupName, Transform head, Transform tail, float radius)
{
ColliderGroup group = default;
foreach (var g in _colliderGroups)
{
if (g.Name == groupName)
{
group = g;
break;
}
}
if (group == null)
{
group = new ColliderGroup { Name = groupName };
_colliderGroups.Add(group);
}
foreach (var collider in group.Colliders)
{
if (collider.transform == head)
{
return;
}
}
var c = GetOrAddComponent<SphereCapsuleCollider>(head.gameObject);
c.Tail = tail;
c.Radius = radius;
c.GizmoColor = GetGizmoColor(group);
group.Colliders.Add(c);
}
static T GetOrAddComponent<T>(GameObject o) where T : Component
{
var t = o.GetComponent<T>();
if (t != null)
{
return t;
}
return o.AddComponent<T>();
}
HashSet<int> _clothUsedParticles = new();
public void InitializeCloth(
StrandGroup g,
ParticleList list,
List<Strand> strands,
List<(SpringConstraint, ClothRectCollision)> clothRects)
{
if (g.Connection == StrandConnectionType.Cloth || g.Connection == StrandConnectionType.ClothLoop)
{
for (int i = 1; i < strands.Count; ++i)
{
var s0 = strands[i - 1];
var s1 = strands[i];
for (int j = 1; j < s0.Particles.Count && j < s1.Particles.Count; ++j)
{
// d x x c
// | |
// a x-x b
var a = s0.Particles[j];
var b = s1.Particles[j];
var c = s1.Particles[j - 1];
var d = s0.Particles[j - 1];
_clothUsedParticles.Add(a.Init.Index);
_clothUsedParticles.Add(b.Init.Index);
_clothUsedParticles.Add(c.Init.Index);
_clothUsedParticles.Add(d.Init.Index);
if (i % 2 == 1)
{
// 互い違いに
// abcd to badc
(a, b) = (b, a);
(c, d) = (d, c);
}
clothRects.Add((
new SpringConstraint(
list._particles[a.Init.Index],
list._particles[b.Init.Index]),
new ClothRectCollision(
a.Init.Index, b.Init.Index, c.Init.Index, d.Init.Index)));
}
}
if (strands.Count >= 3)
{
if (g.Connection == StrandConnectionType.ClothLoop)
{
var i = strands.Count;
var s0 = strands.Last();
var s1 = strands.First();
for (int j = 1; j < s0.Particles.Count && j < s1.Particles.Count; ++j)
{
var a = s0.Particles[j];
var b = s1.Particles[j];
var c = s1.Particles[j - 1];
var d = s0.Particles[j - 1];
_clothUsedParticles.Add(a.Init.Index);
_clothUsedParticles.Add(b.Init.Index);
_clothUsedParticles.Add(c.Init.Index);
_clothUsedParticles.Add(d.Init.Index);
if (i % 2 == 1)
{
// 互い違いに
// abcd to badc
(a, b) = (b, a);
(c, d) = (d, c);
}
clothRects.Add((
new SpringConstraint(
list._particles[a.Init.Index],
list._particles[b.Init.Index]),
new ClothRectCollision(
a.Init.Index, b.Init.Index, c.Init.Index, d.Init.Index
)
));
}
}
}
}
}
public void Initialize()
{
_initialized = true;
foreach (var g in _strandGroups)
{
if (g.Roots?.Count == 0)
{
continue;
}
var strands = new List<Strand>();
foreach (var root in g.Roots)
{
var strand = _list.MakeParticleStrand(Env, root, g.DefaultStrandRaius, g.CollisionMask);
strands.Add(strand);
}
InitializeCloth(g, _list, strands, _clothRects);
_strands.AddRange(strands);
}
_newPos = new(_list._particles.Count);
_list.EndInitialize(_newPos.Init);
_restPositions = new Vector3[_list._particles.Count];
_newPos.EndInitialize();
foreach (var (s, c) in _clothRects)
{
c.InitializeColliderSide(_newPos, _colliderGroups);
}
}
void Start()
{
if (!_initialized)
{
Initialize();
}
}
/// <summary>
/// すべての Particle を Init 状態にする。
/// Verlet の Prev を現在地に更新する(速度0)。
/// </summary>
public void ResetParticle()
{
foreach (var strand in _strands)
{
strand.Reset(_list._particleTransforms);
}
}
public void Update()
{
if (Time.deltaTime == 0)
{
return;
}
Process(Time.deltaTime);
}
public void Process(float deltaTime)
{
using var profile = new ProfileSample("RotateParticle");
using (new ProfileSample("UpdateRoot"))
{
//
// input
//
// 各strandのrootの移動と回転を外部から入力する。
// それらを元に各 joint の方向を元に戻した場合の戻り位置を計算する
foreach (var strand in _strands)
{
strand.UpdateRoot(_list._particleTransforms, _newPos, _restPositions);
}
}
using (new ProfileSample("Verlet"))
{
//
// particle simulation
//
// verlet 積分
var time = new FrameTime(deltaTime);
_list.BeginFrame(Env, time, _restPositions);
foreach (var (spring, collision) in _clothRects)
{
// cloth constraint
spring.Resolve(time, _clothFactor);
}
_list.Verlet(Env, time, _newPos.Init);
// 長さで拘束
foreach (var strand in _strands)
{
strand.ForceLength(_list._particleTransforms, _newPos);
}
}
// collision
using (new ProfileSample("Collision"))
{
for (int i = 0; i < _colliderGroups.Count; ++i)
{
var g = _colliderGroups[i];
foreach (var (spring, rect) in _clothRects)
{
using var prof = new ProfileSample("Collision: Cloth");
// 頂点 abcd は同じ CollisionMask
if (_list._particles[rect._a].Init.CollisionMask.HasFlag((CollisionGroupMask)(i + 1)))
{
// cloth
rect.Collide(_newPos, g.Colliders);
}
}
for (int j = 0; j < _list._particles.Count; ++j)
{
using var prof = new ProfileSample("Collision: Strand");
if (_clothUsedParticles.Contains(j))
{
// 布で処理された
continue;
}
var particle = _list._particles[j];
if (particle.Init.Mass == 0)
{
continue;
}
// 紐の当たり判定
if (particle.Init.CollisionMask.HasFlag((CollisionGroupMask)(i + 1)))
{
var p = _newPos.Get(j);
foreach (var c in g.Colliders)
{
// strand
if (c != null && c.TryCollide(p, particle.Init.Radius, out var resolved))
{
_newPos.CollisionMove(particle.Init.Index, resolved, c.Radius);
}
}
}
}
}
}
using (new ProfileSample("Apply"))
{
for (int i = 0; i < _newPos.CollisionCount.Length; ++i)
{
if (_newPos.CollisionCount[i] > 0)
{
_list._particles[i].HasCollide = true;
}
}
var result = _newPos.Resolve();
//
// apply result
//
// apply positions and
// calc rotation from positions recursive
foreach (var strand in _strands)
{
strand.Apply(_list._particleTransforms, result);
}
}
}
void OnDrawGizmos()
{
_list.DrawGizmos();
foreach (var (spring, rect) in _clothRects)
{
rect.DrawGizmos();
}
if (_newPos != null)
{
_newPos.DrawGizmos();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 67a32b7c5e169f64daa6a8717d3de4f1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,18 @@
using UnityEngine;
namespace RotateParticle
{
[System.Serializable]
public class SimulationEnv
{
public Transform Center;
[Range(0, 1)]
public float Stiffness = 0.1f;
[Range(0, 1)]
public float DragForce = 0.4f;
public Vector3 External = new Vector3(0, -0.001f, 0);
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c9c034f40b6adaf4ba23f456fd5a2ec1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,41 @@
using System;
using UnityEngine;
namespace RotateParticle
{
public class SpringConstraint
{
RotateParticle _p0;
RotateParticle _p1;
// 初期長さ
float _rest;
public SpringConstraint(RotateParticle p0, RotateParticle p1)
{
_p0 = p0;
_p1 = p1;
_rest = Vector3.Distance(p0.State.Current, p1.State.Current);
}
/// <summary>
/// フックの法則
/// </summary>
/// <returns></returns>
public void Resolve(FrameTime time, float hookean)
{
var d = Vector3.Distance(_p0.State.Current, _p1.State.Current);
var f = (d - _rest) * hookean;
var dx = (_p1.State.Current - _p0.State.Current).normalized * f / time.SqDt;
_p0.Force += dx;
_p1.Force -= dx;
}
public void DrawGizmo()
{
Gizmos.DrawLine(_p0.State.Current, _p1.State.Current);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f9f187b56235e024bb4edef8b3bb2142
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,165 @@
using System.Collections.Generic;
using SphereTriangle;
using UnityEngine;
namespace RotateParticle
{
public class Strand
{
public CollisionGroupMask CollisionMask;
public Strand(CollisionGroupMask mask)
{
CollisionMask = mask;
}
public readonly List<RotateParticle> Particles = new();
public void UpdateRoot(IReadOnlyList<Transform> transforms, SphereTriangle.PositionList positions, Vector3[] restPositions)
{
var root = Particles[0];
// only root affected
var t = transforms[root.Init.Index];
root.State.Update(t);
positions.Init(root.Init.Index, root.Init.Mass, t.position);
// Debug.Assert(root.Children.Count == 1);
foreach (var child in root.Children)
{
_CalcRest(transforms, positions.Result, restPositions, root.Children[0]);
}
}
void _CalcRest(IReadOnlyList<Transform> transforms, IReadOnlyList<Vector3> positions,
Vector3[] restPositions,
RotateParticle particle)
{
var restRotation = particle.RestRotation(transforms);
if (particle.Parent != null)
{
// var localPosition = particle.Init.BoneAxis * Vector3.Distance(positions[particle.Parent.Init.Index], positions[particle.Init.Index]);
restPositions[particle.Init.Index] = positions[particle.Parent.Init.Index] + restRotation * particle.Init.LocalPosition;
}
else
{
restPositions[particle.Init.Index] = restRotation * particle.Init.LocalPosition;
}
foreach (var child in particle.Children)
{
_CalcRest(transforms, positions, restPositions, child);
}
}
public void ForceLength(IReadOnlyList<Transform> transforms, PositionList positions)
{
var root = Particles[0];
for (int i = 0; i < root.Children.Count; ++i)
{
_ForceConstraint(transforms, positions, i, root.Children[i], positions.Get(root.Init.Index));
}
}
void _ForceConstraint(IReadOnlyList<Transform> transforms, PositionList positions, int child_index, RotateParticle particle, in Vector3 parent)
{
// update position
Vector3 newPosition;
if (child_index == 0)
{
newPosition = parent + (positions.Get(particle.Init.Index) - parent).normalized * particle.Init.StrandLength;
}
else
{
// 枝分かれ。特別処理
var firstSibling = particle.Parent.Children[0];
var firstPosition = positions.Get(firstSibling.Init.Index);
newPosition = firstPosition + transforms[particle.Parent.Init.Index].rotation * (particle.Init.LocalPosition - firstSibling.Init.LocalPosition);
}
positions.Init(particle.Init.Index, particle.Init.Mass, newPosition);
for (int i = 0; i < particle.Children.Count; ++i)
{
_ForceConstraint(transforms, positions, i, particle.Children[i], newPosition);
}
}
public void Reset(IReadOnlyList<Transform> transforms)
{
var root = Particles[0];
_ResetRecursive(transforms, root);
}
void _ResetRecursive(IReadOnlyList<Transform> transforms,
RotateParticle joint)
{
var t = transforms[joint.Init.Index];
t.localPosition = joint.Init.LocalPosition;
t.localRotation = joint.Init.LocalRotation;
joint.State.Apply(t.position, zeroVelocity: true);
for (int i = 0; i < joint.Children.Count; ++i)
{
_ResetRecursive(transforms, joint.Children[i]);
}
}
public void Apply(IReadOnlyList<Transform> transforms, IReadOnlyList<Vector3> positions)
{
var root = Particles[0];
for (int i = 0; i < root.Children.Count; ++i)
{
var child = root.Children[i];
_ApplyRecursive(transforms, positions, i, child);
}
}
void _ApplyRecursive(IReadOnlyList<Transform> transforms, IReadOnlyList<Vector3> positions,
int child_index, RotateParticle joint)
{
var t = transforms[joint.Init.Index];
if (child_index > 0)
{
// 枝分かれ。mass=0 にもしている。
// self position
t.position = positions[joint.Init.Index];
joint.State.Apply(t.position);
}
else if (joint.Init.Mass > 0)
{
Debug.Assert(joint.Parent != null);
var restRotation = joint.RestRotation(transforms);
Quaternion aimRotation = Quaternion.FromToRotation(
// 初期状態
restRotation * joint.Init.BoneAxis,
// 現状
positions[joint.Init.Index] - positions[joint.Parent.Init.Index]);
var r = aimRotation * restRotation;
// parent rotation
transforms[joint.Parent.Init.Index].rotation = r;
// self position
t.position = positions[joint.Init.Index];
joint.State.Apply(t.position);
}
if (joint.Children.Count > 0)
{
for (int i = 0; i < joint.Children.Count; ++i)
{
_ApplyRecursive(transforms, positions, i, joint.Children[i]);
}
}
else
{
// tail
t.rotation = t.parent.rotation * joint.Init.LocalRotation;
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9aee349d7a2c2344a232a0e158f57dc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: cfa6b58a8a8571e478cf6bac4a00a23d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 7caaf005bf5d55e41860411c0c3d0710
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 36a9770569fe64c4f98dbe384596e4b2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,117 @@
using UnityEngine;
namespace SphereTriangle
{
struct CapsuleInfo
{
public SphereCapsuleCollider Collider;
public Triangle Triangle;
public Vector3 MinOnPlane;
public float MinDistance;
public float MinDistanceClmap;
public Vector3 MinOnPlaneClamp;
public Vector3 MinPos;
public Vector3 MinClamp;
public Vector3 MaxOnPlane;
public float MaxDistance;
public float MaxDistanceClamp;
public Vector3 MaxOnPlaneClamp;
public Vector3 MaxPos;
public Vector3 MaxClamp;
/// <summary>
/// min and max is tail and head.
/// </summary>
public bool Reverse;
public bool Intersected;
public CapsuleInfo(in Triangle t, SphereCapsuleCollider collider)
{
Collider = collider;
Triangle = t;
var headDistance = t.Plane.GetDistanceToPoint(collider.transform.position);
var tailDistance = t.Plane.GetDistanceToPoint(collider.Tail.position);
if (headDistance <= tailDistance)
{
Reverse = false;
MinDistance = headDistance;
MaxDistance = tailDistance;
MinOnPlane = t.Plane.ClosestPointOnPlane(collider.transform.position);
MaxOnPlane = t.Plane.ClosestPointOnPlane(collider.Tail.position);
MinClamp = collider.transform.position;
MaxClamp = collider.Tail.position;
MinPos = collider.transform.position;
MaxPos = collider.Tail.position;
}
else
{
Reverse = true;
MaxDistance = headDistance;
MinDistance = tailDistance;
MaxOnPlane = t.Plane.ClosestPointOnPlane(collider.transform.position);
MinOnPlane = t.Plane.ClosestPointOnPlane(collider.Tail.position);
MaxClamp = collider.transform.position;
MinClamp = collider.Tail.position;
MaxPos = collider.transform.position;
MinPos = collider.Tail.position;
}
// Intersect
Intersected = true;
MinDistanceClmap = MinDistance;
MinOnPlaneClamp = MinOnPlane;
MaxDistanceClamp = MaxDistance;
MaxOnPlaneClamp = MaxOnPlane;
if (MinDistance < -Collider.Radius)
{
if (MaxDistance < -Collider.Radius)
{
Intersected = false;
}
else
{
// clamp Min
MinDistanceClmap = -Collider.Radius;
}
}
else if (MaxDistance > Collider.Radius)
{
if (MinDistance > Collider.Radius)
{
Intersected = false;
}
else
{
// clamp Max
MaxDistanceClamp = Collider.Radius;
}
}
//
MaxOnPlaneClamp = Vector3.Lerp(MinOnPlane, MaxOnPlane, (MaxDistanceClamp - MinDistance) / (MaxDistance - MinDistance));
MinOnPlaneClamp = Vector3.Lerp(MaxOnPlane, MinOnPlane, (MinDistanceClmap - MaxDistance) / (MinDistance - MaxDistance));
MaxClamp = Vector3.Lerp(MinPos, MaxPos, (MaxDistanceClamp - MinDistance) / (MaxDistance - MinDistance));
MinClamp = Vector3.Lerp(MaxPos, MinPos, (MinDistanceClmap - MaxDistance) / (MinDistance - MaxDistance));
}
public void DrawGizmo()
{
Gizmos.color = Color.blue;
Gizmos.DrawLine(Reverse ? Collider.Tail.position : Collider.transform.position, MinOnPlane);
Gizmos.DrawWireSphere(MinOnPlane, 0.01f);
Gizmos.color = Color.blue;
Gizmos.DrawLine(Reverse ? Collider.transform.position : Collider.Tail.position, MaxOnPlane);
Gizmos.DrawWireSphere(MaxOnPlane, 0.01f);
Gizmos.color = Color.magenta;
Gizmos.DrawLine(MinOnPlaneClamp, MaxOnPlaneClamp);
Gizmos.color = Color.green;
Triangle.DrawGizmos();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7a797b9c09763964d84ef1497b260fb7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,244 @@
using System.Collections.Generic;
using UnityEngine;
namespace SphereTriangle
{
public class ClothRectCollision
{
// 2枚の三角形
// abc
// cda
// に対する衝突(球 or カプセル)を管理する
public readonly int _a;
public readonly int _b;
public readonly int _c;
public readonly int _d;
Triangle _triangle0;
float _trinagle0Collision;
Triangle _triangle1;
float _triangle1Collision;
TriangleCapsuleCollisionSolver _s0 = new();
TriangleCapsuleCollisionSolver _s1 = new();
// 各コライダーが初期姿勢で三角形ABCの法線の正か負のどちらにあるのかを記録する
Dictionary<SphereCapsuleCollider, float> _initialColliderNormalSide = new();
/// <summary>
/// two triangles
/// d x-x c
/// |/|
/// a x-x b
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
/// <param name="c"></param>
/// <param name="d"></param>
public ClothRectCollision(
int a, int b, int c, int d)
{
_a = a;
_b = b;
_c = c;
_d = d;
}
public void InitializeColliderSide(PositionList list, IReadOnlyList<ColliderGroup> colliderGroups)
{
var a = list.Get(_a);
var b = list.Get(_b);
var c = list.Get(_c);
var d = list.Get(_d);
// x c
// /|
// a x-x b
var t = new Triangle(a, b, c);
foreach (var g in colliderGroups)
{
foreach (var collider in g.Colliders)
{
var p = t.Plane.ClosestPointOnPlane(collider.transform.position);
var dot = Vector3.Dot(t.Plane.normal, collider.transform.position - p);
_initialColliderNormalSide[collider] = dot;
}
}
}
Bounds GetBoundsFrom4(in Vector3 a, in Vector3 b, in Vector3 c, in Vector3 d)
{
var aabb = new Bounds(a, Vector3.zero);
aabb.Encapsulate(b);
aabb.Encapsulate(c);
aabb.Encapsulate(d);
return aabb;
}
Bounds GetBounds(PositionList list)
{
return GetBoundsFrom4(list.Get(_a), list.Get(_b), list.Get(_c), list.Get(_d));
}
public void Collide(PositionList list, IReadOnlyCollection<SphereCapsuleCollider> colliders)
{
using (new ProfileSample("Rect: Prepare"))
{
_s0.BeginFrame();
_s1.BeginFrame();
var a = list.Get(_a);
var b = list.Get(_b);
var c = list.Get(_c);
var d = list.Get(_d);
// d x-x c
// |/
// a x
_triangle1 = new Triangle(c, d, a);
_triangle1Collision -= 0.1f;
if (_triangle1Collision < 0)
{
_triangle1Collision = 0;
}
// x c
// /|
// a x-x b
_triangle0 = new Triangle(a, b, c);
_trinagle0Collision -= 0.1f;
if (_trinagle0Collision < 0)
{
_trinagle0Collision = 0;
}
}
using (new ProfileSample("Rect: Collide"))
{
var aabb = GetBounds(list);
foreach (var collider in colliders)
{
using (new ProfileSample("EaryOut"))
{
if (!aabb.Intersects(collider.GetBounds()))
{
continue;
}
var p = _triangle0.Plane.ClosestPointOnPlane(collider.transform.position);
var dot = Vector3.Dot(_triangle0.Plane.normal, collider.transform.position - p);
if (_initialColliderNormalSide[collider] * dot < 0)
{
// 片側
continue;
}
}
if (TryCollide(_s0, collider, _triangle0, out var l0))
{
_trinagle0Collision = 1.0f;
list.CollisionMove(_a, l0, collider.Radius);
list.CollisionMove(_b, l0, collider.Radius);
list.CollisionMove(_c, l0, collider.Radius);
}
if (TryCollide(_s1, collider, _triangle1, out var l1))
{
_triangle1Collision = 1.0f;
list.CollisionMove(_c, l1, collider.Radius);
list.CollisionMove(_d, l1, collider.Radius);
list.CollisionMove(_a, l1, collider.Radius);
}
}
}
}
/// <summary>
/// 衝突して移動デルタを得る
/// </summary>
/// <param name="collider"></param>
/// <param name="t"></param>
/// <param name="l"></param>
/// <returns></returns>
static bool TryCollide(TriangleCapsuleCollisionSolver solver, SphereCapsuleCollider collider, in Triangle t, out LineSegment l)
{
if (collider.Tail == null)
{
using var profile = new ProfileSample("Sphere");
// sphere
return TryCollideSphere(t, collider.transform.position, collider.Radius, out l);
}
else
{
// capsule
TriangleCapsuleCollisionSolver.Result result = default;
using (new ProfileSample("Capsule: Collide"))
{
result = solver.Collide(t, collider, new(collider.transform.position, collider.Tail.position), collider.Radius);
}
using (new ProfileSample("Capsule: TryGetClosest"))
{
var type = result.TryGetClosest(out l);
return type.HasValue;
}
}
}
/// <summary>
///
/// </summary>
/// <param name="triangle"></param>
/// <param name="collider"></param>
/// <param name="radius"></param>
/// <returns>collider => 衝突点 への線分を返す</returns>
static bool TryCollideSphere(in Triangle triangle, in Vector3 collider, float radius, out LineSegment l)
{
var p = triangle.Plane.ClosestPointOnPlane(collider);
var distance = Vector3.Distance(p, collider);
if (distance > radius)
{
l = default;
return false;
}
if (triangle.IsSameSide(p))
{
l = new LineSegment(collider, p);
return true;
}
// p を三辺に投影し t を得る
// var proj = triangle.Project(p);
// if (proj.TryGetClosest(collider, out var x))
// {
// // 最近点の距離
// // TODO:
// return new LineSegment(collider, x);
// }
throw new System.NotImplementedException();
}
public void DrawGizmos()
{
if (_triangle0.Points == null)
{
return;
}
var r = Vector3.Distance(_triangle0.b, _triangle0.c) * 0.1f;
_DrawGizmos(_triangle0, _s0, _trinagle0Collision, r);
_DrawGizmos(_triangle1, _s1, _triangle1Collision, r);
#if AABB_DEBUG
Gizmos.matrix = Matrix4x4.identity;
Gizmos.color = Color.cyan;
var aabb = GetBoundsFrom4(_triangle0.a, _triangle0.b, _triangle1.a, _triangle1.b);
Gizmos.DrawWireCube(aabb.center, aabb.size);
#endif
}
void _DrawGizmos(in Triangle t, TriangleCapsuleCollisionSolver solver, float collision, float radius)
{
solver.DrawGizmos(t, collision, radius);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c16dea6c47fa81d4184547a209d8fe61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SphereTriangle
{
[Serializable]
public class ColliderGroup
{
[SerializeField]
public string Name;
[SerializeField]
public List<SphereCapsuleCollider> Colliders = new List<SphereCapsuleCollider>();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 07647b77d9934cd42b0bd0ab880ed321
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,20 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FrameRate : MonoBehaviour
{
[Range(15, 200)]
public int _FrameRate = 30;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
Application.targetFrameRate = _FrameRate;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39108f27e89d7514d86e56e2ffb0550c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,58 @@
using UnityEngine;
namespace SphereTriangle
{
public class LineDistanceGizmo : MonoBehaviour
{
public SphereCapsuleCollider LineA;
public SphereCapsuleCollider LineB;
void Reset()
{
if (LineA == null)
{
LineA = GetComponent<SphereCapsuleCollider>();
}
}
public void OnDrawGizmos()
{
if (LineA?.Tail != null && LineB?.Tail != null)
{
var a = new LineSegment(LineA.transform.position, LineA.Tail.position);
var b = new LineSegment(LineB.transform.position, LineB.Tail.position);
var (s, t) = LineSegment.CalcClosest(a, b);
var a_s = a.GetPoint(s);
var b_t = b.GetPoint(t);
Gizmos.color = Color.magenta;
Gizmos.DrawLine(a_s, b_t);
Gizmos.color = Color.gray;
if (s < 0)
{
Gizmos.DrawLine(LineA.transform.position, a_s);
}
else if (s > 1)
{
Gizmos.DrawLine(LineA.Tail.position, a_s);
}
else
{
Gizmos.DrawWireSphere(a_s, 0.05f);
}
if (t < 0)
{
Gizmos.DrawLine(LineB.transform.position, b_t);
}
else if (t > 1)
{
Gizmos.DrawLine(LineB.transform.position, b_t);
}
else
{
Gizmos.DrawWireSphere(b_t, 0.05f);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cceeecd3d3db5cd4ba74dfa6490e38b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,160 @@
using UnityEngine;
namespace SphereTriangle
{
public readonly struct LineSegment
{
public readonly Vector3 Start;
public readonly Vector3 End;
public Vector3 Vector => End - Start;
public Ray Ray => new Ray(Start, Vector);
public float Length => Vector.magnitude;
public float SqLength => Vector.sqrMagnitude;
public LineSegment(in Vector3 start, in Vector3 end) => (Start, End) = (start, end);
public Vector3 GetPoint(float t)
{
return Start + Vector * t;
}
/// <summary>
/// 球との衝突時の移動ベクトル
/// Start が球の中心。End 衝突点。
/// </summary>
/// <param name="radius"></param>
/// <returns></returns>
public Vector3 GetDelta(float radius)
{
return Vector.normalized * (radius - Length);
}
public float Project(in Vector3 p)
{
var dir = p - Start;
return Vector3.Dot(Vector.normalized, dir) / dir.magnitude;
}
// P(s)
// Q(t)
public static (float s, float t) CalcClosest(in LineSegment p, in LineSegment q)
{
var d_p2 = Vector3.Dot(p.Vector, p.Vector);
var d_q2 = Vector3.Dot(q.Vector, q.Vector);
var d_pq = Vector3.Dot(p.Vector, q.Vector);
var d_q_p_p = Vector3.Dot(q.Start - p.Start, p.Vector);
var d_p_q_q = Vector3.Dot(p.Start - q.Start, q.Vector);
var denom = d_p2 * d_q2 - d_pq * d_pq;
if (denom < 1e-4)
{
var pq = q.Start - p.Start;
var d = Vector3.Dot(p.Vector, pq);
var s = d / p.Length;
return (s, 0);
}
else
{
var f = 1 / denom;
var s = d_q2 * d_q_p_p + d_pq * d_p_q_q;
var t = d_pq * d_q_p_p + d_p2 * d_p_q_q;
return (f * s, f * t);
}
}
// public static bool Intersect(in LineSegment p, in LineSegment q, out float s, out float t)
// {
// (s, t) = CalcClosest(p, q);
// if (s >= 0.0 && s <= 1.0 && t >= 0.0 && t <= 1.0)
// {
// if (Vector3.Distance(p.GetPoint(s), q.GetPoint(t)) < 1e4f)
// {
// return true;
// }
// }
// return false;
// }
public bool TryClampPlaneDistance(in Plane p, float distance, out LineSegment clamped, out Vector3 o)
{
var ray = Ray;
var hit = p.Raycast(ray, out var t);
if (!hit && t == 0)
{
// 平行
clamped = default;
o = default;
return false;
}
o = ray.GetPoint(t);
var vs = Start - o;
var ve = End - o;
var ds = p.GetDistanceToPoint(Start);
var de = p.GetDistanceToPoint(End);
var ts = 1.0f;
var te = 1.0f;
if (ds > 0)
{
if (de > 0)
{
//+s
//+e
//==
if (ds > distance && de > distance)
{
clamped = default;
return false;
}
ts = Mathf.Min(distance, ds) / ds;
te = Mathf.Min(distance, de) / de;
}
else
{
//+s
//==
//-e
ts = Mathf.Min(distance, ds) / ds;
te = Mathf.Max(-distance, de) / de;
}
}
else
{
if (de < 0)
{
//==
//-s
//-e
if (ds < -distance && de < -distance)
{
clamped = default;
return false;
}
ts = Mathf.Max(-distance, ds) / ds;
te = Mathf.Max(-distance, de) / de;
}
else
{
//+e
//==
//-s
ts = Mathf.Max(-distance, ds) / ds;
te = Mathf.Min(distance, de) / de;
}
}
var cs = new Ray(o, vs).GetPoint(vs.magnitude * ts);
var ce = new Ray(o, ve).GetPoint(ve.magnitude * te);
clamped = new(cs, ce);
return true;
}
public void DrawGizmos(float radius = 0.01f)
{
Gizmos.DrawLine(Start, End);
Gizmos.DrawWireSphere(Start, radius);
Gizmos.DrawWireSphere(End, radius);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: add9de55aee3f344cab64a1c99c7c6aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SphereTriangle
{
public delegate void InitPosition(int index, float mass, in Vector3 position);
public class PositionList
{
// 初期状態
// 質点の重さ(0は移動しない)
float[] Mass;
// 衝突前
Vector3[] Positions;
public int[] CollisionCount;
// 衝突による移動距離
public Vector3[] Delta;
// Positions に Delta を反映した結果
public Vector3[] Result;
public PositionList(int count)
{
Positions = new Vector3[count];
CollisionCount = new int[count];
Mass = new float[count];
Delta = new Vector3[count];
Result = new Vector3[count];
}
public Vector3 Get(int index)
{
return Positions[index];
}
public void Init(int index, float mass, in Vector3 pos)
{
Mass[index] = mass;
Positions[index] = pos;
CollisionCount[index] = 0;
Delta[index] = Vector3.zero;
}
public void EndInitialize()
{
// Buffer.BlockCopy(Positions, 0, Result, 0, Positions.Length);
for (int i = 0; i < Positions.Length; ++i)
{
Result[i] = Positions[i];
}
}
/// <summary>
/// 衝突した。移動を蓄積
/// </summary>
/// <param name="index"></param>
/// <param name="pos"></param>
public void CollisionMove(int index, in LineSegment l, float radius, float factor = 1.0f)
{
using var profile = new ProfileSample("CollisionMove");
if (Mass[index] > 0)
{
Delta[index] += l.GetDelta(radius) * factor;
++CollisionCount[index];
}
}
public IReadOnlyList<Vector3> Resolve()
{
for (int i = 0; i < Positions.Length; ++i)
{
if (CollisionCount[i] > 0)
{
Result[i] = Positions[i] + Delta[i] / CollisionCount[i];
}
else
{
Result[i] = Positions[i];
}
}
return Result;
}
/// <summary>
/// 衝突前後の状態を描画
/// </summary>
public void DrawGizmos()
{
Gizmos.color = Color.red;
for (int i = 0; i < Positions.Length; ++i)
{
if (CollisionCount[i] > 0)
{
Gizmos.DrawLine(Positions[i], Result[i]);
// Gizmos.DrawWireSphere(Positions[i], 0.01f);
// Gizmos.DrawSphere(Result[i], 0.01f);
}
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 836b32456872256409962a684cbee1b4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,21 @@
using System;
using UnityEngine.Profiling;
namespace SphereTriangle
{
public struct ProfileSample : IDisposable
{
string _name;
public ProfileSample(string name)
{
_name = name;
Profiler.BeginSample(name);
}
public void Dispose()
{
Profiler.EndSample();
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8375e9fc2aee4f448e349401c0bf068
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,176 @@
using UnityEngine;
namespace SphereTriangle
{
[DisallowMultipleComponent]
public class SphereCapsuleCollider : MonoBehaviour
{
[SerializeField, Range(0.01f, 1f)]
public float Radius = 0.05f;
public Transform Tail;
public Ray? HeadTailRay => Tail == null ? null : new Ray { origin = transform.position, direction = (Tail.position - transform.position) };
public float CapsuleLength => Tail == null ? 0 : Vector3.Distance(Tail.position, transform.position);
public Color GizmoColor = Color.yellow;
public bool SolidGizmo = false;
public Bounds GetBounds()
{
if (Tail == null)
{
return new Bounds(transform.position, new Vector3(Radius, Radius, Radius));
}
var h = transform.position;
var t = Tail.position;
var d = h - t;
var aabb = new Bounds((h + t) * 0.5f, new Vector3(Mathf.Abs(d.x), Mathf.Abs(d.y), Mathf.Abs(d.z)));
aabb.Expand(Radius * 2);
return aabb;
}
public void Reset()
{
if (transform.childCount > 0)
{
Tail = transform.GetChild(0);
}
}
/// <summary>
/// collide sphere a and sphere b.
/// move sphere b to resolved if collide.
/// </summary>
/// <param name="from"></param>
/// <param name="ra"></param>
/// <param name="to"></param>
/// <param name="ba"></param>
/// <param name="resolved"></param>
/// <returns></returns>
static bool TryCollideSphereAndSphere(
in Vector3 from, float ra,
in Vector3 to, float rb,
out LineSegment resolved
)
{
var d = Vector3.Distance(from, to);
if (d > (ra + rb))
{
resolved = default;
return false;
}
Vector3 normal = (to - from).normalized;
resolved = new(from, from + normal * (d - rb));
return true;
}
/// <summary>
/// collide capsule and sphere b.
/// move sphere b to resolved if collide.
/// </summary>
/// <param name="capsuleHead"></param>
/// <param name="capsuleTail"></param>
/// <param name="capsuleRadius"></param>
/// <param name="b"></param>
/// <param name="rb"></param>
static bool TryCollideCapsuleAndSphere(
in Vector3 capsuleHead,
in Vector3 capsuleTail,
float capsuleRadius,
in Vector3 b,
float rb,
out LineSegment resolved
)
{
var P = (capsuleTail - capsuleHead).normalized;
var Q = b - capsuleHead;
var dot = Vector3.Dot(P, Q);
if (dot <= 0)
{
// head側半球の球判定
return TryCollideSphereAndSphere(capsuleHead, capsuleRadius, b, rb, out resolved);
}
var t = dot / P.magnitude;
if (t >= 1.0f)
{
// tail側半球の球判定
return TryCollideSphereAndSphere(capsuleTail, capsuleRadius, b, rb, out resolved);
}
// head-tail上の m_transform.position との最近点
var p = capsuleHead + P * t;
return TryCollideSphereAndSphere(p, capsuleRadius, b, rb, out resolved);
}
/// <summary>
/// collision for strand
/// </summary>
/// <param name="p"></param>
/// <param name="radius"></param>
/// <param name="resolved"></param>
/// <returns></returns>
public bool TryCollide(in Vector3 p, float radius, out LineSegment resolved)
{
if (Tail != null)
{
return TryCollideCapsuleAndSphere(transform.position, Tail.position, this.Radius, p, radius, out resolved);
}
else
{
return TryCollideSphereAndSphere(transform.position, this.Radius, p, radius, out resolved);
}
}
public static void DrawCapsuleGizmo(Vector3 start, Vector3 end, float radius)
{
var tail = end - start;
var distance = (end - start).magnitude;
Gizmos.matrix = Matrix4x4.TRS(start, Quaternion.FromToRotation(Vector3.forward, tail), Vector3.one);
Gizmos.DrawWireSphere(Vector3.zero, radius);
Gizmos.DrawWireSphere(Vector3.forward * distance, radius);
var capsuleEnd = Vector3.forward * distance;
var offsets = new Vector3[] { new Vector3(-1.0f, 0.0f, 0.0f), new Vector3(0.0f, 1.0f, 0.0f), new Vector3(1.0f, 0.0f, 0.0f), new Vector3(0.0f, -1.0f, 0.0f) };
for (int i = 0; i < offsets.Length; i++)
{
Gizmos.DrawLine(offsets[i] * radius, capsuleEnd + offsets[i] * radius);
}
Gizmos.matrix = Matrix4x4.identity;
}
public void OnDrawGizmos()
{
if (SolidGizmo)
{
Gizmos.color = Color.white;
Gizmos.DrawSphere(transform.position, Radius);
}
Gizmos.color = GizmoColor;
if (transform.parent)
{
Gizmos.DrawLine(transform.parent.position, transform.position);
}
if (Tail != null)
{
DrawCapsuleGizmo(transform.position, Tail.position, Radius);
}
else
{
Gizmos.DrawWireSphere(transform.position, Radius);
}
#if AABB_DEBUG
Gizmos.matrix = Matrix4x4.identity;
Gizmos.color = Color.magenta;
var aabb = GetBounds();
Gizmos.DrawWireCube(aabb.center, aabb.size);
#endif
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4d8072930ebf6d34ea30547c75dfb378
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,14 @@
{
"name": "SphereTriangle",
"rootNamespace": "",
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": false,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 0138a2c7639266f45b948ea162125de5
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SphereTriangle
{
public enum StrandConnectionType
{
Cloth,
ClothLoop,
Strand,
}
[System.Flags]
public enum CollisionGroupMask : uint
{
None = 0,
Group01 = 0x00000001,
Group02 = 0x00000002,
Group03 = 0x00000004,
Group04 = 0x00000008,
Group05 = 0x00000010,
Group06 = 0x00000020,
Group07 = 0x00000040,
Group08 = 0x00000080,
Group09 = 0x00000100,
Group10 = 0x00000200,
Group11 = 0x00000400,
Group12 = 0x00000800,
Group13 = 0x00001000,
Group14 = 0x00002000,
Group15 = 0x00004000,
Group16 = 0x00008000,
Group17 = 0x00010000,
Group18 = 0x00020000,
Group19 = 0x00040000,
Group20 = 0x00080000,
Group21 = 0x00100000,
Group22 = 0x00200000,
Group23 = 0x00400000,
Group24 = 0x00800000,
Group25 = 0x01000000,
Group26 = 0x02000000,
Group27 = 0x04000000,
Group28 = 0x08000000,
Group29 = 0x10000000,
Group30 = 0x20000000,
Group31 = 0x40000000,
Group32 = 0x80000000,
All = uint.MaxValue,
}
[Serializable]
public class StrandGroup
{
[SerializeField]
public string Name;
[SerializeField]
public CollisionGroupMask CollisionMask;
[SerializeField]
public StrandConnectionType Connection;
[SerializeField]
public List<Transform> Roots = new List<Transform>();
[SerializeField]
[Range(0.001f, 0.5f)]
public float DefaultStrandRaius = 0.05f;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: efab592ca762fea46adb3c38b41e65d0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using UnityEngine;
namespace SphereTriangle
{
public class TransformSort : IComparer<Transform>
{
Vector3 _o;
public TransformSort(in Vector3 origin)
{
_o = origin;
}
static int Rot180(int d)
{
// d -= 180;
while (d < 0)
{
d += 360;
}
return d;
}
public int Degree(Transform t)
{
var x = t.position.x - _o.x;
var y = t.position.z - _o.z;
var a = (int)(Mathf.Rad2Deg * Mathf.Atan2(x, y));
a = Rot180(a);
// Debug.Log($"{t.name} => {a}");
return a;
}
public int Compare(Transform x, Transform y)
{
return Degree(x) - Degree(y);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: dcbd60313a3b8e34fbe465e9bcbcab98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,285 @@
using UnityEngine;
namespace SphereTriangle
{
/// <summary>
/// 三角形との交差が 2 => 交点 と 交点
/// 三角形との交差が 1 => 交点 と 三角形内の線分端点
/// 三角形との交差が 0 => 線分の端点が両方とも三角形内の場合。端点 と 端点
/// </summary>
public struct TriangleSegmentIntersection
{
public float t0;
public float t1;
public TriangleSegmentIntersection(float _t0, float _t1)
{
if (_t0 <= _t1)
{
t0 = _t0;
t1 = _t1;
}
else
{
t0 = _t1;
t1 = _t0;
}
}
}
public struct Triangle
{
public Plane Plane;
public Vector3[] Points;
public Vector3 a => Points[0];
public Vector3 b => Points[1];
public Vector3 c => Points[2];
public Triangle(Vector3 a, Vector3 b, Vector3 c)
{
Plane = new Plane(a, b, c);
Points = new Vector3[] { a, b, c };
}
/// <summary>
/// p がすべての辺の同じ側(内積的な意味で)にある => 三角形の内側にある
/// </summary>
/// <param name="p"></param>
/// <returns></returns>
public bool IsSameSide(Vector3 p)
{
var da = Vector3.Dot(Vector3.Cross(p - a, b - a), Plane.normal);
var db = Vector3.Dot(Vector3.Cross(p - b, c - b), Plane.normal);
var dc = Vector3.Dot(Vector3.Cross(p - c, a - c), Plane.normal);
if (da > 0)
{
if (db > 0)
{
if (dc > 0)
{
return true;
}
}
}
else if (da < 0)
{
if (db < 0)
{
if (dc < 0)
{
return true;
}
}
}
return false;
}
// /// <summary>
// /// p から lerp の係数を計算
// /// </summary>
// /// <param name="p0"></param>
// /// <param name="p1"></param>
// /// <param name="p"></param>
// /// <returns></returns>
// static float getT(in Vector3 p0, in Vector3 p1, in Vector3 p)
// {
// return (p - p0).magnitude / (p1 - p0).magnitude;
// }
// /// <summary>
// /// 三角形と線分の交差を判定する。
// /// </summary>
// /// <param name="p0">線分始点(t=0)。三角形 abc と同一平面を期待</param>
// /// <param name="p1">線分終点(t=1)。三角形 abc と同一平面を期待</param>
// /// <returns></returns>
// public bool TryIntersectSegment(in Vector3 p0, in Vector3 p1, out TriangleSegmentIntersection intersection)
// {
// // [ab bc ca]
// // [ab bc] [bc ca] [ca ab]
// // [ab] [bc] [ca]
// // []
// if (TryIntersectSegments(p0, p1, a, b, out var ab))
// {
// if (TryIntersectSegments(p0, p1, b, c, out var bc))
// {
// // [ab bc] or [ab bc ca]
// intersection = new TriangleSegmentIntersection(getT(p0, p1, ab), getT(p0, p1, bc));
// return true;
// }
// else
// {
// if (TryIntersectSegments(p0, p1, c, a, out var ca))
// {
// // [ab ca]
// intersection = new TriangleSegmentIntersection(getT(p0, p1, ab), getT(p0, p1, ca));
// return true;
// }
// else
// {
// // [ab]
// if (IsSameSide(p0))
// {
// intersection = new TriangleSegmentIntersection(0, getT(p0, p1, ab));
// return true;
// }
// else
// {
// intersection = new TriangleSegmentIntersection(getT(p0, p1, ab), 1.0f);
// return true;
// }
// }
// }
// }
// else
// {
// if (TryIntersectSegments(p0, p1, b, c, out var bc))
// {
// if (TryIntersectSegments(p0, p1, c, a, out var ca))
// {
// // [bc ca]
// intersection = new TriangleSegmentIntersection(getT(p0, p1, bc), getT(p0, p1, ca));
// return true;
// }
// else
// {
// // [bc]
// if (IsSameSide(p0))
// {
// intersection = new TriangleSegmentIntersection(0, getT(p0, p1, bc));
// return true;
// }
// else
// {
// intersection = new TriangleSegmentIntersection(getT(p0, p1, bc), 1.0f);
// return true;
// }
// }
// }
// else
// {
// if (TryIntersectSegments(p0, p1, c, a, out var ca))
// {
// // [ca]
// if (IsSameSide(p0))
// {
// intersection = new TriangleSegmentIntersection(0, getT(p0, p1, ca));
// return true;
// }
// else
// {
// intersection = new TriangleSegmentIntersection(getT(p0, p1, ca), 1.0f);
// return true;
// }
// }
// else
// {
// // []
// if (IsSameSide(p0) && IsSameSide(p1))
// {
// intersection = new TriangleSegmentIntersection(0, 1);
// return true;
// }
// else
// {
// intersection = default;
// return false;
// }
// }
// }
// }
// }
// /// <summary>
// /// 線分と線分の交差を判定する
// /// a-b d
// /// /
// /// c
// /// https://qiita.com/zu_rin/items/09876d2c7ec12974bc0f
// /// </summary>
// /// <param name="a"></param>
// /// <param name="b"></param>
// /// <param name="c"></param>
// /// <param name="d"></param>
// /// <returns></returns>
// static bool TryIntersectSegments(in Vector3 a, in Vector3 b, in Vector3 c, in Vector3 d, out Vector3 p)
// {
// var deno = Vector3.Cross(b - a, d - c).magnitude;
// if (deno < 1e-5)
// {
// // 線分が平行
// p = default;
// return false;
// }
// var s = Vector3.Cross(c - a, d - c).magnitude / deno;
// var t = Vector3.Cross(b - a, a - c).magnitude / deno;
// if (s < 0.0 || 1.0 < s || t < 0.0 || 1.0 < t)
// {
// // 線分が交差していない
// p = default;
// return false;
// }
// p = new Vector3(
// a.x + s * (b - a).x,
// a.y + s * (b - a).y,
// a.z + s * (b - a).z
// );
// return true;
// }
// public (Vector3, float) ProjectAB(in Vector3 p)
// {
// var ab = (b - a).normalized;
// var d = Vector3.Dot(ab, (p - a));
// var x = a + ab * d;
// return (x, d / (b - a).magnitude);
// }
// public (Vector3, float) ProjectBC(in Vector3 p)
// {
// var bc = (c - b).normalized;
// var d = Vector3.Dot(bc, (p - b));
// var x = b + bc * d;
// return (x, d / (c - b).magnitude);
// }
// public (Vector3, float) ProjectCA(in Vector3 p)
// {
// var ca = (a - c).normalized;
// var d = Vector3.Dot(ca, (p - c));
// var x = c + ca * d;
// return (x, d / (a - c).magnitude);
// }
// public TriangleProjection Project(in Vector3 p)
// {
// var (ab, ta) = ProjectAB(p);
// var (bc, tb) = ProjectBC(p);
// var (ca, tc) = ProjectCA(p);
// return new TriangleProjection
// {
// ab = ab,
// ta = ta,
// bc = bc,
// tb = tb,
// ca = ca,
// tc = tc,
// };
// }
public void DrawGizmos()
{
#if UNITY_2022_3_OR_NEWER
Gizmos.DrawLineStrip(Points, true);
#else
for (int i = 0; i < Points.Length - 1; ++i)
{
Gizmos.DrawLine(Points[i], Points[i + 1]);
}
Gizmos.DrawLine(Points[Points.Length - 1], Points[0]);
#endif
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2558c27234db0c741a6821682205da57
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,366 @@
// #define USE_VERTEX_DISTANCE
using System;
using System.Collections.Generic;
using UnityEngine;
namespace SphereTriangle
{
[Serializable]
public class TriangleCapsuleCollisionSolver
{
public enum IntersectionType
{
PlaneInTriangle,
#if USE_VERTEX_DISTANCE
VertexDistance,
#endif
EdgeDistance,
}
public struct Result
{
/// <summary>
/// カプセルStartの投影点が三角形内部
/// </summary>
public LineSegment? StartInTriangle;
/// <summary>
/// カプセルEndの投影点が三角形内部
/// </summary>
public LineSegment? EndInTriangle;
#if USE_VERTEX_DISTANCE
/// <summary>
/// カプセル線分と頂点Aの距離が半径以内
/// </summary>
public LineSegment? VertexADistance;
/// <summary>
/// カプセル線分と頂点Bの距離が半径以内
/// </summary>
public LineSegment? VertexBDistance;
/// <summary>
/// カプセル線分と頂点Cの距離が半径以内
/// </summary>
public LineSegment? VertexCDistance;
#endif
/// <summary>
/// カプセル線分と辺ABの距離が半径以内
/// </summary>
public LineSegment? EdgeABDistance;
/// <summary>
/// カプセル線分と辺BCの距離が半径以内
/// </summary>
public LineSegment? EdgeBCDistance;
/// <summary>
/// カプセル線分と辺CAの距離が半径以内
/// </summary>
public LineSegment? EdgeCADistance;
public IntersectionType? TryGetClosest(out LineSegment value)
{
using var profile = new ProfileSample("TryGetClosest");
var d = float.PositiveInfinity;
value = default;
IntersectionType? it = default;
if (StartInTriangle.HasValue && StartInTriangle.Value.SqLength < d)
{
value = StartInTriangle.Value;
it = IntersectionType.PlaneInTriangle;
}
if (EndInTriangle.HasValue && EndInTriangle.Value.SqLength < d)
{
value = EndInTriangle.Value;
it = IntersectionType.PlaneInTriangle;
}
#if USE_VERTEX_DISTANCE
if (VertexADistance.HasValue) yield return (IntersectionType.VertexDistance, VertexADistance.Value);
if (VertexBDistance.HasValue) yield return (IntersectionType.VertexDistance, VertexBDistance.Value);
if (VertexCDistance.HasValue) yield return (IntersectionType.VertexDistance, VertexCDistance.Value);
#endif
if (EdgeABDistance.HasValue && EdgeABDistance.Value.SqLength < d)
{
value = EdgeABDistance.Value;
it = IntersectionType.EdgeDistance;
}
if (EdgeBCDistance.HasValue && EdgeBCDistance.Value.SqLength < d)
{
value = EdgeBCDistance.Value;
it = IntersectionType.EdgeDistance;
}
if (EdgeCADistance.HasValue && EdgeCADistance.Value.SqLength < d)
{
value = EdgeCADistance.Value;
it = IntersectionType.EdgeDistance;
}
return it;
}
};
public class Status
{
/// <summary>
/// Capsule clamped by radius distance from plane
/// </summary>
public LineSegment Clamped;
/// <summary>
/// capsule line intersect plane
/// </summary>
public Vector3 O;
/// <summary>
/// capsule Start project plane
/// </summary>
public Vector3 PS;
/// <summary>
/// capsule End project plane
/// </summary>
public Vector3 PE;
public int CollisionCount = 0;
public Result Result;
}
// 複数コライダーのデバッグ表示のため
public Dictionary<SphereCapsuleCollider, Status> collider_status_map = new();
public void BeginFrame()
{
collider_status_map.Clear();
}
public Result Collide(in Triangle t, SphereCapsuleCollider collider, in LineSegment capsule, float radius)
{
if (collider == null)
{
throw new ArgumentNullException("collider");
}
Status status = default;
using (new ProfileSample("Parallel Prepare"))
{
if (collider_status_map.TryGetValue(collider, out status))
{
}
else
{
status = new Status();
collider_status_map[collider] = status;
}
float dot = default;
using (new ProfileSample("Dot"))
{
dot = Vector3.Dot(t.Plane.normal, capsule.Vector);
}
if (Mathf.Abs(dot) < 1e-4)
{
using var profile = new ProfileSample("Parallel");
// 三角面とカプセルが平行
var d = t.Plane.GetDistanceToPoint(capsule.Start);
if (d < -radius || d > radius)
{
return default;
}
// 距離による clamp できない
status.Clamped = capsule;
}
else
{
using var profile = new ProfileSample("TryClampPlaneDistance");
if (capsule.TryClampPlaneDistance(t.Plane, radius, out status.Clamped, out status.O))
{
}
else
{
return default;
}
}
using (new ProfileSample("ClosestPointOnPlane"))
{
status.PS = t.Plane.ClosestPointOnPlane(status.Clamped.Start);
status.PE = t.Plane.ClosestPointOnPlane(status.Clamped.End);
}
}
LineSegment? startInTriangle = default;
LineSegment? endInTriangle = default;
using (new ProfileSample("InTriangle"))
{
if (t.IsSameSide(status.PS))
{
startInTriangle = new(status.Clamped.Start, status.PS);
++status.CollisionCount;
}
if (t.IsSameSide(status.PE))
{
endInTriangle = new(status.Clamped.End, status.PE);
++status.CollisionCount;
}
}
#if USE_VERTEX_DISTANCE
var vertexADistance = calcVertexDistance(t.a, status.Clamped, radius);
if (vertexADistance.HasValue)
{
++status.CollisionCount;
}
var vertexBDistance = calcVertexDistance(t.b, status.Clamped, radius);
if (vertexBDistance.HasValue)
{
++status.CollisionCount;
}
var vertexCDistance = calcVertexDistance(t.c, status.Clamped, radius);
if (vertexCDistance.HasValue)
{
++status.CollisionCount;
}
#endif
LineSegment? edgeABDistance = default;
LineSegment? edgeBCDistance = default;
LineSegment? edgeCADistance = default;
using (new ProfileSample("EdgeDistance"))
{
// triangle edges
var ab = new LineSegment(t.a, t.b);
var bc = new LineSegment(t.b, t.c);
var ca = new LineSegment(t.c, t.a);
edgeABDistance = calcEdgeDistance(ab, status.Clamped, radius);
if (edgeABDistance.HasValue)
{
++status.CollisionCount;
}
edgeBCDistance = calcEdgeDistance(bc, status.Clamped, radius);
if (edgeBCDistance.HasValue)
{
++status.CollisionCount;
}
edgeCADistance = calcEdgeDistance(ca, status.Clamped, radius);
if (edgeCADistance.HasValue)
{
++status.CollisionCount;
}
}
status.Result = new Result
{
StartInTriangle = startInTriangle,
EndInTriangle = endInTriangle,
#if USE_VERTEX_DISTANCE
VertexADistance = vertexADistance,
VertexBDistance = vertexBDistance,
VertexCDistance = vertexCDistance,
#endif
EdgeABDistance = edgeABDistance,
EdgeBCDistance = edgeBCDistance,
EdgeCADistance = edgeCADistance,
};
return status.Result;
}
LineSegment? calcVertexDistance(in Vector3 a, in LineSegment clamped, float radius)
{
var t = clamped.Project(a);
if (t < 0 || t > 1)
{
return default;
}
var p = clamped.GetPoint(t);
if (Vector3.Distance(a, p) > radius)
{
return default;
}
return new LineSegment(p, a);
}
LineSegment? calcEdgeDistance(in LineSegment ab, in LineSegment clamped, float radius)
{
var (a_s, a_t) = LineSegment.CalcClosest(ab, clamped);
// clamp range
a_s = Mathf.Clamp(a_s, 0, 1);
a_t = Mathf.Clamp(a_t, 0, 1);
var ab_s = ab.GetPoint(a_s);
var ab_t = clamped.GetPoint(a_t);
var distance_a = (ab_s - ab_t).magnitude;
if (distance_a > radius)
{
return default;
}
return new LineSegment(ab_t, ab_s);
}
Color GetGizmoColor(IntersectionType type, float color)
{
var g = new Color(0.3f, 0.3f, 0.3f);
return Color.Lerp(g, type.ToColor(), color);
}
public void DrawGizmos(in Triangle t, float collision, float radius)
{
Gizmos.matrix = Matrix4x4.identity;
var hit = false;
foreach (var (_, status) in collider_status_map)
{
var type = status.Result.TryGetClosest(out var l);
if (!type.HasValue)
{
continue;
}
// capsule
hit = true;
Gizmos.color = GetGizmoColor(type.Value, collision);
t.DrawGizmos();
l.DrawGizmos();
Gizmos.DrawSphere(l.End, radius);
Gizmos.DrawWireSphere(l.Start, radius);
}
if (!hit)
{
Gizmos.color = Color.gray;
t.DrawGizmos();
}
}
}
public static class IntersectionTypeExtensions
{
public static Color ToColor(this TriangleCapsuleCollisionSolver.IntersectionType type)
{
switch (type)
{
case TriangleCapsuleCollisionSolver.IntersectionType.PlaneInTriangle:
return Color.magenta;
#if USE_VERTEX_DISTANCE
case TriangleCapsuleCollisionSolver.IntersectionType.VertexDistance:
return Color.blue;
#endif
case TriangleCapsuleCollisionSolver.IntersectionType.EdgeDistance:
return Color.cyan;
default:
throw new Exception();
}
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c4efecee94290be43b3affbcc6bd191a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,64 @@
using UnityEngine;
namespace SphereTriangle
{
public class TriangleCapsuleGizmo : MonoBehaviour
{
public Transform B;
public Transform C;
public SphereCapsuleCollider Capsule;
void Reset()
{
for (int i = 0; i < transform.childCount; ++i)
{
switch (i)
{
case 0:
if (this.B == null) B = transform.GetChild(i);
break;
case 1:
if (this.C == null) C = transform.GetChild(i);
break;
}
}
}
[SerializeField]
TriangleCapsuleCollisionSolver _solver = new();
public void OnDrawGizmos()
{
if (B == null) return;
if (C == null) return;
var t = new Triangle(transform.position, B.position, C.position);
if (Capsule?.Tail == null) return;
var capsule = new LineSegment(Capsule.transform.position, Capsule.Tail.position);
_solver.BeginFrame();
var result = _solver.Collide(t, Capsule, capsule, Capsule.Radius);
var type = result.TryGetClosest(out var l);
if (!type.HasValue)
{
Gizmos.color = Color.gray;
t.DrawGizmos();
return;
}
_solver.DrawGizmos(t, 1.0f, Capsule.Radius);
Gizmos.color = Color.magenta;
Gizmos.DrawLine(l.Start, l.End);
Gizmos.DrawSphere(l.End, 0.01f);
Gizmos.DrawWireSphere(l.Start, 0.02f);
var delta = l.Vector.normalized * (Capsule.Radius - l.Length);
Gizmos.color = Color.green;
Gizmos.DrawLine(l.End, l.End + delta);
Gizmos.DrawSphere(l.End + delta, 0.01f);
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c201feca8fd8c644b987521e89706c4f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: