前幾天做小球融合,讓我陷入困難,後來搜到用貝塞爾曲線模擬中間過渡效果的,但是他們的代碼都有顯然的和隱藏的錯誤之處,改了半天都無法達到預期效果,不夠直白易懂,但是經過一天的琢磨最後還是搞懂了思路並想到了算法,源碼和項目也分享給大家學習,註釋很詳細,所以我只簡單解釋思路,示意圖:
如圖,根據兩球的距離設置合適的a1和a2,求出P1,P2,P3,P4的座標,然後根據圓上切線的方向,自己根據圓的半徑和兩圓的距離,求出沿切線方向的切點座標,即可得出繪製貝塞爾曲線的四個座標點,特判一些特殊情況,加入融合時修改半徑的功能即可。不知道核心代碼怎麼使用的話可以下載項目運行看看。
效果圖:
把算法放到processing上運行,效率也很高:
unity2018.4 demo項目:https://pan.baidu.com/s/1u8GF-ziL3C84PDK_0N9Q9A 密碼:2333
核心代碼:
using UnityEngine;
public class Curve : MonoBehaviour {
public int lineNum; //曲線分段數
public Material line; //材質顏色
public Transform c1, c2; //圓
public Transform p1, p2, p3, p4; //圓上的切點
public Transform h1, h2, h3, h4; //切線延伸方向的點
private Vector2 P1, P2, P3, P4, H1, H2, H3, H4; //8個點座標
private float initAngle = 40; //初始不相交時的兩圓切點相對於各自圓心的夾角
private float tangentLength1, tangentLength2; //切線長度
private float scaleRate = 0.01f; //圓融合時面積變化比例
private void Update() {
Vector2 scale = Metaball(c1.position, c2.position, c1.localScale.x * 2.5f, c2.localScale.x * 2.5f);
SetScale(scale);
}
//計算8點位置
private Vector2 Metaball(Vector2 c1, Vector2 c2, float r1, float r2) {
Vector2 ans = new Vector2();
float dis = Vector2.Distance(c1, c2); //兩圓心的距離
if (dis < r1 + r2) { //吸收小圓
ans.x = r1 * (1 - scaleRate); //小圓新半徑
ans.y = Mathf.Sqrt(r2 * r2 + r1 * r1 - ans.x * ans.x); //大圓新半徑
}
else { //分離,不吸收
ans = new Vector2(r1, r2);
}
float tiltAngle1, tiltAngle2; //兩圓心的相對角度
tiltAngle1 = Vector2.Angle(c2 - c1, new Vector2(1, 0)); //angle函數返回夾角絕對值,下一句手動判斷正負
if ((c2 - c1).y < 0) tiltAngle1 = -tiltAngle1;
tiltAngle2 = Limit(tiltAngle1 + 180);
float angle1, angle2, angle3, angle4; //四個切點相對於圓心的角度
angle1 = angle2 = angle3 = angle4 = 0;
float tanAngle1, tanAngle2, tanAngle3, tanAngle4; //四個切線方向的角度
if (dis > r1 + r2) { //兩圓不相交
angle1 = Limit(tiltAngle1 + initAngle);
angle2 = Limit(tiltAngle1 - initAngle);
angle3 = Limit(tiltAngle2 - initAngle);
angle4 = Limit(tiltAngle2 + initAngle);
tangentLength1 = r1 * dis / 20;
if (tangentLength1 > r1 / 4) tangentLength1 = r1 / 4;
tangentLength2 = r2 * dis / 20;
if (tangentLength2 > r2 / 4) tangentLength2 = r2 / 4;
}
else if (r1 < dis + r2 && r2 < dis + r1) { //兩圓相交
float interAngle1, interAngle2; //相交點相對於圓心的角度
interAngle1 = Mathf.Acos((r1 * r1 - r2 * r2 + dis * dis) / (2 * dis * r1)) * Mathf.Rad2Deg; //推導出的公式
interAngle2 = Mathf.Acos((r2 * r2 - r1 * r1 + dis * dis) / (2 * dis * r2)) * Mathf.Rad2Deg;
angle1 = Limit(tiltAngle1 + interAngle1 + initAngle);
angle2 = Limit(tiltAngle1 - interAngle1 - initAngle);
angle3 = Limit(tiltAngle2 - interAngle2 - initAngle);
angle4 = Limit(tiltAngle2 + interAngle2 + initAngle);
tangentLength1 = r1 / 4;
tangentLength2 = r2 / 4;
}
tanAngle1 = Limit(angle1 - 90);
tanAngle2 = Limit(angle2 + 90);
tanAngle3 = Limit(angle3 + 90);
tanAngle4 = Limit(angle4 - 90);
P1 = c1 + new Vector2(Mathf.Cos(angle1 * Mathf.Deg2Rad), Mathf.Sin(angle1 * Mathf.Deg2Rad)) * r1;
P2 = c1 + new Vector2(Mathf.Cos(angle2 * Mathf.Deg2Rad), Mathf.Sin(angle2 * Mathf.Deg2Rad)) * r1;
P3 = c2 + new Vector2(Mathf.Cos(angle3 * Mathf.Deg2Rad), Mathf.Sin(angle3 * Mathf.Deg2Rad)) * r2;
P4 = c2 + new Vector2(Mathf.Cos(angle4 * Mathf.Deg2Rad), Mathf.Sin(angle4 * Mathf.Deg2Rad)) * r2;
H1 = P1 + new Vector2(Mathf.Cos(tanAngle1 * Mathf.Deg2Rad), Mathf.Sin(tanAngle1 * Mathf.Deg2Rad)) * tangentLength1;
H2 = P2 + new Vector2(Mathf.Cos(tanAngle2 * Mathf.Deg2Rad), Mathf.Sin(tanAngle2 * Mathf.Deg2Rad)) * tangentLength1;
H3 = P3 + new Vector2(Mathf.Cos(tanAngle3 * Mathf.Deg2Rad), Mathf.Sin(tanAngle3 * Mathf.Deg2Rad)) * tangentLength2;
H4 = P4 + new Vector2(Mathf.Cos(tanAngle4 * Mathf.Deg2Rad), Mathf.Sin(tanAngle4 * Mathf.Deg2Rad)) * tangentLength2;
/*
P1 = p1.position;
P2 = p2.position;
P3 = p3.position;
P4 = p4.position;
H1 = h1.position;
H2 = h2.position;
H3 = h3.position;
H4 = h4.position;
*/
SetPointPos(P1, P2, P3, P4, H1, H2, H3, H4);
if (r1 < dis + r2 && r2 < dis + r1) { //圓沒有互相包含時
DrawCurve();
}
return ans;
}
//設置小球新半徑
private void SetScale(Vector2 scale) {
c1.localScale = new Vector3(1, 1, 1) * scale.x / 2.5f;
c2.localScale = new Vector3(1, 1, 1) * scale.y / 2.5f;
}
//限制角度在-180到180之間
private float Limit(float angle) {
if (angle > 180) return angle - 360;
if (angle <= -180) return angle + 360;
return angle;
}
//顯示8個點的位置
private void SetPointPos(Vector2 P1, Vector2 P2, Vector2 P3, Vector2 P4, Vector2 H1, Vector2 H2, Vector2 H3, Vector2 H4) {
p1.position = P1;
p2.position = P2;
p3.position = P3;
p4.position = P4;
h1.position = H1;
h2.position = H2;
h3.position = H3;
h4.position = H4;
}
//編輯器界面繪製貝塞爾曲線
private void DrawCurve() {
float x;
Vector2 A, B, C, D;
Vector2[] Mpos = new Vector2[6];
Vector2 last = Vector2.zero;
A = P2;
B = H2;
C = H4;
D = P4;
x = 0;
for (int i = 0; i <= lineNum; i++) {//貝塞爾曲線
Mpos[0] = A + (B - A) * x;
Mpos[1] = B + (C - B) * x;
Mpos[2] = C + (D - C) * x;
Mpos[3] = Mpos[0] + (Mpos[1] - Mpos[0]) * x;
Mpos[4] = Mpos[1] + (Mpos[2] - Mpos[1]) * x;
Mpos[5] = Mpos[3] + (Mpos[4] - Mpos[3]) * x;
if (i != 0) {
Debug.DrawLine(last, Mpos[5], Color.white);
}
x += 1.0f / lineNum;
last = Mpos[5];
}
A = P1;
B = H1;
C = H3;
D = P3;
x = 0;
for (int i = 0; i <= lineNum; i++) {//貝塞爾曲線
Mpos[0] = A + (B - A) * x;
Mpos[1] = B + (C - B) * x;
Mpos[2] = C + (D - C) * x;
Mpos[3] = Mpos[0] + (Mpos[1] - Mpos[0]) * x;
Mpos[4] = Mpos[1] + (Mpos[2] - Mpos[1]) * x;
Mpos[5] = Mpos[3] + (Mpos[4] - Mpos[3]) * x;
if (i != 0) {
Debug.DrawLine(last, Mpos[5], Color.white);
}
x += 1.0f / lineNum;
last = Mpos[5];
}
}
//遊戲界面繪製貝塞爾曲線
private void OnPostRender() {
float x;
Vector2 A, B, C, D;
Vector2[] Mpos = new Vector2[6];
Vector2 last = Vector2.zero;
GL.PushMatrix();
GL.LoadPixelMatrix();
line.SetPass(0);
GL.Begin(GL.LINES);
A = P2;
B = H2;
C = H4;
D = P4;
x = 0;
for (int i = 0; i <= lineNum; i++) {//貝塞爾曲線
Mpos[0] = A + (B - A) * x;
Mpos[1] = B + (C - B) * x;
Mpos[2] = C + (D - C) * x;
Mpos[3] = Mpos[0] + (Mpos[1] - Mpos[0]) * x;
Mpos[4] = Mpos[1] + (Mpos[2] - Mpos[1]) * x;
Mpos[5] = Mpos[3] + (Mpos[4] - Mpos[3]) * x;
Vector2 pos = Camera.main.WorldToScreenPoint(Mpos[5]);
if (i == 0) {
GL.Vertex3(pos.x, pos.y, 0);
}
else {
GL.Vertex3(last.x, last.y, 0);
}
GL.Vertex3(pos.x, pos.y, 0);
x += 1.0f / lineNum;
last = pos;
}
GL.End();
GL.Begin(GL.LINES);
A = P1;
B = H1;
C = H3;
D = P3;
x = 0;
for (int i = 0; i <= lineNum; i++) {//貝塞爾曲線
Mpos[0] = A + (B - A) * x;
Mpos[1] = B + (C - B) * x;
Mpos[2] = C + (D - C) * x;
Mpos[3] = Mpos[0] + (Mpos[1] - Mpos[0]) * x;
Mpos[4] = Mpos[1] + (Mpos[2] - Mpos[1]) * x;
Mpos[5] = Mpos[3] + (Mpos[4] - Mpos[3]) * x;
Vector2 pos = Camera.main.WorldToScreenPoint(Mpos[5]);
if (i == 0) {
GL.Vertex3(pos.x, pos.y, 0);
}
else {
GL.Vertex3(last.x, last.y, 0);
}
GL.Vertex3(pos.x, pos.y, 0);
x += 1.0f / lineNum;
last = pos;
}
GL.End();
GL.PopMatrix();
}
}