<style> section { padding: 35px 70px; } </style> <!-- _class: lead --> # 20Q3Q4 VR Breakthrough Kevin, Wallace, Harrison ![bg opacity](https://i.imgur.com/dkwo2qk.gif) --- <!-- _class: lead --> ### Bending physic ![height:600px](https://i.imgur.com/INlyiAV.png) --- <!-- _class: lead --> ### What is Shear Force ![height:600px](https://i.imgur.com/DGQTiiM.png) --- ### Euler–Bernoulli beam theory > The Euler–Bernoulli equation describes the relationship between the beam's deflection and the applied load: ![w:600](https://i.imgur.com/Rb8QNU0.png) * 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 --> ![height:600px](https://i.imgur.com/SdD6SB8.png) $$ \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 --> ![height:400px](https://i.imgur.com/BhJJVCF.png) ```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 --- ![bg](https://i.imgur.com/2ArXBVp.png) ![bg](https://i.imgur.com/9ZKeSLl.png) --- | use on Chain | use on Beam | :-------------------------:|:-------------------------: ![height:500px](https://i.imgur.com/wCArwMh.gif) | ![height:500px](https://i.imgur.com/0rpUwog.png) --- ![bg w:600](https://i.imgur.com/uENz43G.png) ![bg w:600](https://i.imgur.com/dZN3kmI.png) --- ## 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 (註:建模非本日重點) ![height:450px](https://i.imgur.com/HZp8TDp.png) --- ### Exporting rod 因為釣竿的變形會交由 Euler–Bernoulli 方程式計算,因此配合上述 node,在設定骨架時使用 subdivide 平均切成十等分,並自動分配權重。 ![height:400px](https://i.imgur.com/DLzhsNZ.png) --- ### Exporting rod ![height:600px](https://i.imgur.com/Za2KN74.gif) --- ### Importing rod to Unity 將釣竿模型載入Unity,並將Rigging後的骨架丟入RodStrip.cs取代原先測試用的nodeList圓球 ![](https://i.imgur.com/sm35mOu.png) --- ### Importing rod to Unity 實測時會產生閃電狀的變形,切換顯示方塊後可以發現,是因為node沒有依照方程式變形後的方向套用旋轉 ![](https://i.imgur.com/fscG1Ib.gif) --- <!-- _class: lead --> ### Node rotation ![height:500px](https://i.imgur.com/jPUmpQc.png) --- ### 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 ![height:300px](https://i.imgur.com/dwyvTqe.png) $$ \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 | :-------------------------:|:-------------------------: ![height:350px](https://i.imgur.com/8fFqqEF.png) | ![height:350px](https://i.imgur.com/JElojY7.png) --- ### 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風格的釣魚遊戲並不需要如此精確的角度計算 ![height:200px](https://i.imgur.com/OE94CCB.jpg) --- ### 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 ![](https://i.imgur.com/izbEwXz.gif) --- <!-- _class: lead --> ## Water shader ![bg opacity:80%](https://i.imgur.com/JAsIT6a.gif) --- ### Stylized Water 插件 ![height:500px](https://i.imgur.com/t0xQzDE.png) --- ### Migrating to URP <!-- _class: lead--> OceanSurfaceShader.shader (CG) ### &darr; Stylized Water (ShaderGraph) + Gerstner Wave (HLSL) --- ### Migrating to URP ![height:500px](https://i.imgur.com/iEtkrpX.png) --- ### 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 ![height:500px](https://i.imgur.com/JAsIT6a.gif) --- ## Obi 系列插件 ![](https://i.imgur.com/vmR3pP8.png) --- ## Whats I learn form obi rope 在VR釣魚裡面的釣魚線使用obi rope模擬 使用插件之餘,從他提供的應用層code 了解一下constaint如何應用成繩子 以及為什麼obi rope是如何降低他的消耗 --- ## 繩子效果 ![](https://s3.gifyu.com/images/Apr-23-2021-11-57-22.gif) --- ## Data struture of rope node 用稱作Blueprint的資料格式去儲存繩子本身的質量分布等資訊 用關鍵節點去平攤後,再生成每一個繩子點位的particle 可以用來進行constarint模擬以及碰撞 (碰撞也是obi自己另外處理,但以collider作為接口,可以讓boardphase由Unity處理, 自己只要處理需要的narrow phase,是常見的插件做法) ---- ## Data struture of rope node ![](https://i.imgur.com/B5bjChc.png) --- ## 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 ![](https://i.imgur.com/3ETXY4a.png) --- ## Constraint solver 基本上會基於兩個步驟 Apply & Initialize 一個是自己寫的小Solver腳本,一個是box2d ![](https://i.imgur.com/knizM3r.png) --- ## Obi Solver Global Settings ![](https://i.imgur.com/oFT63u7.png) --- ## Obi Solver DLL裡面埋藏的秘密 ![](https://i.imgur.com/ykkfA1K.png) --- ## 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架構 ![](https://i.imgur.com/uD3Qm6H.png) --- ## 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本身計算 ![](https://i.imgur.com/qYfTnJW.png) --- ## Distance Constraint Example ![](https://i.imgur.com/w0DckuA.png) ---- ## Distance Constraint Example ![](https://i.imgur.com/fOhZkIX.png) --- ## 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}]"}
    544 views