<style>
section {
padding: 35px 70px;
}
</style>
<!-- _class: lead -->
# 20Q3Q4 VR Breakthrough
Kevin, Wallace, Harrison

---
<!-- _class: lead -->
### Bending physic

---
<!-- _class: lead -->
### What is Shear Force

---
### Euler–Bernoulli beam theory
> The Euler–Bernoulli equation describes the relationship between the beam's deflection and the applied load:

* M: Bending Moment Q: Shear Force
* E (elastric model) * I (area moment of inertia ) = **Bending stiffness(constant)**
* w = beam’s deflection, q = applied load
---
<!-- _class: lead -->

$$
\begin{eqnarray}
W(x) = x^2 * (3L - x)
\end{eqnarray}
$$
---
<!-- _class: lead -->
$$
\begin{eqnarray}
W(x) = x^2 * (3L - x)
\end{eqnarray}
$$
$$
\begin{eqnarray}
W(mx) = 2(L^3)
\end{eqnarray}
$$
$$
\begin{eqnarray}
Ratio = x^2 * (3L - x) /2(L^3)
\end{eqnarray}
$$
---
<!-- _class: lead -->

```csharp
public static float GetBernoulliPosX(float x,float length)
{
return Mathf.Pow(x, 2) * (3 * length - x) / (2 * Mathf.Pow(l, 3));
}
```
---
<!--
_class: lead invert
-->
## Configure Joint & Nodes
---


---
| use on Chain | use on Beam |
:-------------------------:|:-------------------------:
 | 
---


---
## Define Node
```csharp
private void Init()
{
diffvector = EndTransform.position - RootTransform.position;
Vector3 lastpos = RootTransform.localPosition * RootTransform.lossyScale.x;
nodes = new List<StripNode>();
float l = (EndTransform.position - RootTransform.position).magnitude;
float diffl = l / (float) (nodeList.Count - 1);
for (int i = 0; i < nodeList.Count; i++)
{
StripNode tempnode = new StripNode();
tempnode.Obj = nodeList[i];
tempnode.OriPos = lastpos;
lastpos += diffvector / nodeList.Count;
tempnode.Width = StripUtils.GetBernoulliPosX(i * diffl, l);
nodes.Add(tempnode);
}
}
```
---
## Update nodes position
```csharp
private void UpdateNode(Vector3 targetPos)
{
...
Vector3 endnodediff = targetPos - (lastnode.OriPos + RootTransform.position);
for (int i = 0; i < length; i++)
{
nodes[i].Trans.position = nodes[i].OriPos
+ RootTransform.position
+ ((float) i / length) * endnodediff * nodes[i].Width;
}
...
}
```
*WIP: Rotate issue...*
---
<!--
_class: lead invert
-->
## Rod model export and implement
---
### Exporting rod
Modded and Rigged in Blender (註:建模非本日重點)

---
### Exporting rod
因為釣竿的變形會交由 Euler–Bernoulli 方程式計算,因此配合上述 node,在設定骨架時使用 subdivide 平均切成十等分,並自動分配權重。

---
### Exporting rod

---
### Importing rod to Unity
將釣竿模型載入Unity,並將Rigging後的骨架丟入RodStrip.cs取代原先測試用的nodeList圓球

---
### Importing rod to Unity
實測時會產生閃電狀的變形,切換顯示方塊後可以發現,是因為node沒有依照方程式變形後的方向套用旋轉

---
<!-- _class: lead -->
### Node rotation

---
### Node rotation - Recap
```csharp
void FixedUpdate()
{
for (int i = 0; i < nodes.Count; i++)
{
nodes[i].OriPos =
Vector3.Lerp(RootTransform.position, OriTransform.position, (float) i / nodes.Count) -
RootTransform.position;
}
this.UpdateNode(EndTransform.position);
}
private void UpdateNode(Vector3 targetPos)
{
...
Vector3 endnodediff = targetPos - (lastnode.OriPos + RootTransform.position);
for (int i = 0; i < length; i++)
{
nodes[i].Trans.position = nodes[i].OriPos
+ RootTransform.position
+ ((float) i / length) * endnodediff * nodes[i].Width;
}
...
}
```
---
### Node rotation

$$
\begin{eqnarray}
OriTransform.position &=& O \nonumber \\
EndTransform.position &=& E \nonumber \\
RootTransform.position &=& R \nonumber \\
\end{eqnarray}
$$
---
### Node rotation
```csharp
nodes[i].Trans.position = nodes[i].OriPos + RootTransform.position +
((float) i /length) * endnodediff * nodes[i].Width;
```
$$
\begin{eqnarray}
Pos(i) &=& OriPos(i) + R + \frac{i}{n} * endnodediff * Width(i) \nonumber \\
\end{eqnarray}
$$
理論上來說,我們只要歸納出決定座標的方程式 $Pos(i)$,就可以對它做一次微分,取得決定旋轉方向向量的公式 $Pos'(i)$
---
### Node rotation - Breaking down
1. OriPos: 每個node的相對座標(以釣竿握把R為原點)
```csharp
nodes[i].OriPos = Vector3.Lerp(RootTransform.position, OriTransform.position, i / nodes.Count)
- RootTransform.position
```
$$
\begin{eqnarray}
OriPos(i) &=& Vector3.Lerp(R,O,\frac{i}{n}) - R \nonumber \\
&=& (O - R) * \frac{i}{n} - R \nonumber \\
\end{eqnarray}
$$
---
### Node rotation - Breaking down
2. endnodediff: 釣竿末端node的偏移向量
```csharp
Vector3 endnodediff = targetPos - (lastnode.OriPos + RootTransform.position);
```
$$
\begin{eqnarray}
targetPos &=& EndTransform.position \nonumber \\
&=& E \nonumber \\
endnodediff &=& E - (OriPos(n - 1) + R) \nonumber \\
&=& E - ((O-R)*\frac{n-1}{n} -R + R) \nonumber \\
&=& E - ((O-R)*\frac{n-1}{n}) \nonumber \\
\end{eqnarray}
$$
---
### Node rotation - Breaking down
3. Width: 每個node向endnodediff偏移的比例
```csharp
public static float GetWidth(float x,float l)
{
return Mathf.Pow(x, 2) * (3 * l - x) / (2 * Mathf.Pow(l, 3));
}
float L = (EndTransform.position - RootTransform.position).magnitude;
float diffL = L / (float) (nodeList.Count - 1);
nodes[i].Width = GetWidth(i * diffL, L)
```
---
### Node rotation - Breaking down
3. Width: 每個node向endnodediff偏移的比例
$$
\begin{eqnarray}
Width(i) &=& \frac{(i * \frac{L}{n-1})^2 * (3L-i*\frac{L}{n-1})}{2L^3}\nonumber \\
&=& \frac{(\frac{i}{n-1})^2 * (3 - (\frac{i}{n-1}))}{2}\nonumber \\
&=& \frac{3}{2} (\frac{i}{n-1})^2 - \frac{1}{2} (\frac{i}{n-1})^3 \nonumber \\
\end{eqnarray}
$$
---
### Node rotation
```csharp
nodes[i].Trans.position = nodes[i].OriPos + RootTransform.position +
((float) i /length) * endnodediff * nodes[i].Width;
```
$$
\begin{eqnarray}
Pos(i) &=& OriPos(i) + R + \frac{i}{n} * endnodediff * Width(i) \nonumber \\
&=& ((O - R) * \frac{i}{n} - R) + R + \frac{i}{n} * (E-((O-R) * \frac{n-1}{n})) * Width(i) \nonumber \\
&=& \frac{i}{n} * ((O - R) + (E-((O-R) * \frac{n-1}{n})) * Width(i))\nonumber \\
&=& \frac{i}{n} * ((O - R) + (E-((O-R) * \frac{n-1}{n})) * (\frac{3}{2} (\frac{i}{n-1})^2 - \frac{1}{2} (\frac{i}{n-1})^3))\nonumber \\
\end{eqnarray}
$$
---
### Node rotation
將上述公式推導出來後,試著代入線上的[公式計算機](https://www.derivative-calculator.net/)看看結果
$$
x / n * ((O - R) + (T - ((n - 1) / n) * (O - R)) * (3/2 * (x / (n - 1)) ^ 2 - 1/2 * (x / (n - 1)) ^ 3))
$$
| 靜態 | 向上彎曲5 |
:-------------------------:|:-------------------------:
 | 
---
### Node rotation
取以上公式的一次微分(過程略)
$$
Pos'(i)=\dfrac{\left(t-\frac{\left(n-1\right)\left(o-r\right)}{n}\right)\left(\frac{3x^2}{2\left(n-1\right)^2}-\frac{x^3}{2\left(n-1\right)^3}\right)-r+o}{n}+\dfrac{\left(t-\frac{\left(n-1\right)\left(o-r\right)}{n}\right)x\left(\frac{3x}{\left(n-1\right)^2}-\frac{3x^2}{2\left(n-1\right)^3}\right)}{n}
$$
將 $O$, $R$, $T$ 替換成三維向量,即可套用在各個node上,作為旋轉方向的參考向量,但Low-poly風格的釣魚遊戲並不需要如此精確的角度計算

---
### Node rotation - simplify
為了在遊戲中更流暢地呈現釣竿的擺動,我們退一步使用 `UpdateNode()` 中已經求得的各個Node座標,設定每一個Node指向下一個Node
```csharp
private void UpdateNode(Vector3 targetPos)
{
...
for (int i = 0; i < length - 1; i++)
{
var dirUp = nodes[i + 1].Trans.position - nodes[i].Trans.position;
var dirForward = Vector3.Cross(Vector3.right, dirUp);
nodes[i].Trans.rotation = Quaternion.LookRotation(dirForward, dirUp);
}
var lastUp = targetPos - lastnode.Trans.position; //Endpoint has no node, manually rotate last node
var lastForward = Vector3.Cross(Vector3.right, lastUp);
lastnode.Trans.rotation = Quaternion.LookRotation(lastForward, lastUp);
}
```
---
### Node rotation - result

---
<!-- _class: lead -->
## Water shader

---
### Stylized Water 插件

---
### Migrating to URP
<!-- _class: lead-->
OceanSurfaceShader.shader (CG)
### ↓
Stylized Water (ShaderGraph) + Gerstner Wave (HLSL)
---
### Migrating to URP

---
### Migrating to URP
```GLSL
float3 GerstnerWave(
float direction, float4 waveParam, float3 position, inout float3 tangent, inout float3 binormal)
{
// actual implementation...
}
void GerstnerWaves_float(
float3 pos, float4 waveA, float4 waveB, float4 waveC, float4 waveD, bool displayMultiple, float4 dir, out float3 Offset, out float3 normal)
{
Offset = 0;
float3 tangent = float3(1, 0, 0);
float3 binormal = float3(0, 0, 1);
Offset += GerstnerWave(dir.x, waveA, pos, tangent, binormal);
if (displayMultiple) {
Offset += GerstnerWave(dir.y, waveB, pos, tangent, binormal);
Offset += GerstnerWave(dir.z, waveC, pos, tangent, binormal);
Offset += GerstnerWave(dir.w, waveD, pos, tangent, binormal);
}
normal = normalize(cross(binormal, tangent));
}
```
---
### Water Shader - Result

---
## Obi 系列插件

---
## Whats I learn form obi rope
在VR釣魚裡面的釣魚線使用obi rope模擬
使用插件之餘,從他提供的應用層code
了解一下constaint如何應用成繩子
以及為什麼obi rope是如何降低他的消耗
---
## 繩子效果

---
## Data struture of rope node
用稱作Blueprint的資料格式去儲存繩子本身的質量分布等資訊
用關鍵節點去平攤後,再生成每一個繩子點位的particle
可以用來進行constarint模擬以及碰撞
(碰撞也是obi自己另外處理,但以collider作為接口,可以讓boardphase由Unity處理,
自己只要處理需要的narrow phase,是常見的插件做法)
----
## Data struture of rope node

---
## BluePrint Structure
```csharp
public abstract class ObiActorBlueprint : ScriptableObject, IObiParticleCollection
{
...
/**Particle components*/
[HideInInspector] public Vector3[] positions = null; /**< Particle positions.*/
[HideInInspector] public Vector4[] restPositions = null; /**< Particle rest positions, used to filter collisions.*/
[HideInInspector] public Quaternion[] orientations = null; /**< Particle orientations.*/
[HideInInspector] public Quaternion[] restOrientations = null; /**< Particle rest orientations.*/
[HideInInspector] public Vector3[] velocities = null; /**< Particle velocities.*/
[HideInInspector] public Vector3[] angularVelocities = null; /**< Particle angular velocities.*/
[HideInInspector] public float[] invMasses = null; /**< Particle inverse masses*/
[HideInInspector] public float[] invRotationalMasses = null;
...
```
---
## Center of Obi : constraint based physics
Obi的Solver本體隱藏在Oni.dll裡面,
包含其他幾種的Obi似乎都是用同個constraint solver,所以都包含同一包dll們
個別販售的是用這組solver所組合變化的實作(稱為ObiActor)
----
## Center of Obi : constraint based physics

---
## Constraint solver
基本上會基於兩個步驟 Apply & Initialize
一個是自己寫的小Solver腳本,一個是box2d

---
## Obi Solver
Global Settings

---
## Obi Solver
DLL裡面埋藏的秘密

---
## Obi Solver
**ObiSolver.cs**
```csharp
public sealed class ObiSolver : MonoBehaviour
{
...
public IObiJobHandle BeginStep(float stepTime)
public IObiJobHandle Substep(float substepTime)
public void EndStep(float substepTime)
...
}
```
---
## Obi Solver
```csharp
public sealed class ObiSolver : MonoBehaviour
{
protected void Substep(float substepDeltaTime)
{
using (m_SubstepPerfMarker.Auto())
{
// Necessary when using multiple substeps:
ObiColliderWorld.GetInstance().UpdateWorld();
// Grab rigidbody info:
ObiColliderWorld.GetInstance().UpdateRigidbodies(solvers, substepDeltaTime);
List<IObiJobHandle> handles = new List<IObiJobHandle>();
// Kick off all solver jobs:
foreach (ObiSolver solver in solvers)
if (solver != null)
handles.Add(solver.Substep(substepDeltaTime));
// wait for all solver jobs to complete:
foreach (IObiJobHandle handle in handles)
if (handle != null)
handle.Complete();
// Update rigidbody velocities:
ObiColliderWorld.GetInstance().UpdateRigidbodyVelocities(solvers);
}
}
}
```
---
## Obi Solver
看到IJobHandler架構

---
## Obi是怎麼用Solver的
**ObiActor.cs**
Obi的應用(Rope, Rod, Cloth etc)的基本class,代表一系列的particle的集合以及他們的控制
```csharp
public abstract class ObiActor : MonoBehaviour, IObiParticleCollection
{
...
public virtual void BeginStep(float stepTime)
{
UpdateCollisionMaterials();
if (OnBeginStep != null)
OnBeginStep(this, stepTime);
}
public virtual void Substep(float substepTime)
{
if (OnSubstep != null)
OnSubstep(this, substepTime);
}
public virtual void EndStep(float substepTime)
{
if (OnEndStep != null)
OnEndStep(this, substepTime);
}
...
```
---
## 繩子的Solver
**ObiRopeBase.cs**
繩子和桿子都是繼承Rope Base,
都包含在這組插件裡面,實作共用算particle長度等的公式,
但沒有實作substep / init的部分
---
## 繩子的Solver
**ObiRopeBase.cs**
```csharp
public abstract class ObiRopeBase : ObiActor
{
...
public float CalculateLength()
public void RecalculateRestLength()
...
}
```
---
## 繩子的Solver
**ObiRope.cs**
```csharp
public class ObiRope : ObiRopeBase, IDistanceConstraintsUser, IBendConstraintsUser
{
...
public override void Substep(float substepTime)
{
base.Substep(substepTime);
if (isActiveAndEnabled)
ApplyTearing(substepTime);
}
...
}
```
---
## 繩子的Solver
```csharp
public class ObiRope : ObiRopeBase, IDistanceConstraintsUser, IBendConstraintsUser
{
...
protected void ApplyTearing(float substepTime)
{
...
if (dc != null && sc != null)
for (int j = 0; j < dc.GetBatchCount(); ++j)
{
var batch = dc.GetBatch(j) as ObiDistanceConstraintsBatch;
var solverBatch = sc.batches[j] as ObiDistanceConstraintsBatch;
for (int i = 0; i < batch.activeConstraintCount; i++)
{
int elementIndex = j + 2 * i;
// divide lambda by squared delta time to get force in newtons:
int offset = solverBatchOffsets[(int)Oni.ConstraintType.Distance][j];
float force = solverBatch.lambdas[offset + i] / sqrTime;
elements[elementIndex].constraintForce = force;
...
}
}
}
...
}
```
---
## lambda是誰
lambda計算藏在DLL裡面,就是constraint本身計算

---
## Distance Constraint Example

----
## Distance Constraint Example

---
## Rope Render
因為時間不夠,渲染part就大概整理概念
Obi使用blueprint選擇的節點生成raw chunk
再用Chainkin's algorithm將控制節點生成 smooth chunk
最後用smooth chunk生成extruded mesh貼上貼圖
---
## Smoother
```csharp
private void AllocateRawChunks(ObiRopeBase actor)
{
rawChunks.Clear();
if (actor.path == null)
return;
// Count particles for each chunk.
int particles = 0;
for (int i = 0; i < actor.elements.Count; ++i)
{
particles++;
// At discontinuities, start a new chunk.
if (i < actor.elements.Count - 1 && actor.elements[i].particle2 != actor.elements[i + 1].particle1)
{
AllocateChunk(++particles);
particles = 0;
}
}
AllocateChunk(++particles);
}
```
---
## Smoother
```csharp
public void GenerateSmoothChunks(ObiRopeBase actor, uint smoothingLevels)
{
...
for (int i = 0; i < rawChunks.Count; ++i)
{
// get smooth curve points:
Chaikin(rawChunks[i], smoothChunks[i], smoothingLevels);
}
...
}
```
---
## 結論
* Obi 在繩子的物理模擬部分只有做distance joint
* 繩子的碰撞使用particle(node)進行節點碰撞
* 雖然constatint本身是黑盒子,作法本身大同小異可以想像
* Obi用起來模擬的效果很不錯,使用參數tune起來可以達成蠻多效果
* distance joint加上verlet integration網路上蠻多教學的,模擬本身成本還好
* Obi在渲染繩子下了不少功夫,重新刻輪子比較大的挑戰可能在這邊
---
## Reference
https://box2d.org/files/ErinCatto_ModelingAndSolvingConstraints_GDC2009.pdf
https://github.com/dci05049/Verlet-Rope-Unity/blob/master/Tutorial%20Verlet%20Rope/Assets/Rope.cs
{"metaMigratedAt":"2023-06-15T22:59:45.582Z","metaMigratedFrom":"YAML","title":"20Q3Q4 VR Breakthrough","breaks":true,"contributors":"[{\"id\":\"3f96bfe3-95e9-4c3b-ae43-04b1178c88b0\",\"add\":11390,\"del\":8515},{\"id\":\"ae86858e-3a6a-48f9-82a0-4f7edef8284e\",\"add\":24721,\"del\":18384},{\"id\":\"cbd92717-3bef-45f9-9acd-931bfb0b1682\",\"add\":102871,\"del\":96154},{\"id\":null,\"add\":421,\"del\":13},{\"id\":\"93822543-6496-4a8c-98c3-0f7d280c09eb\",\"add\":7518,\"del\":6439}]"}