如何判斷一個點在三角形內部

如何判斷一個點在三角形內部

基本思路

三角示例

如圖,點P在三角形ABC內部,可以通過以下三個條件判斷:

  1. 點P和點C在直線AB同側
  2. 點P和點B在直線AC同側
  3. 點P和點A在直線BC同側

如果以上三個條件同時滿足,則點P在三角形ABC內部。

下面將會用到叉乘這個數學工具來確定一個點在直線的哪一側。

判斷點在直線的哪一側

叉乘是一個判斷點在直線哪一側的數學工具。先看一下叉乘的定義:

a⃗ ×b⃗ =a⃗ b⃗ sinθn⃗ 

其中,θ 爲向量夾角,n⃗  是一個向量,與a⃗ b⃗  都垂直,方向滿足右手螺旋法則,即下圖所示:

右手螺旋法則

於是,從第一個向量的方向看,如果第二個向量在左邊,那個叉乘是正的,在右邊,則是負的,在同一個方向上,則是0.叉乘的大小,則是兩個向量組成的平行四邊形的面積。

那麼叉乘具體如何計算呢?先將x、y、z軸方向的單位向量分別記爲i⃗ j⃗ k⃗  ,則如果有兩個向量,分別爲:

u⃗ =u1i⃗ +u2j⃗ +u3k⃗ =(u1,u2,u3)v⃗ =v1i⃗ +v2j⃗ +v3k⃗ =(v1,v2,v3)

則有:
u⃗ ×v⃗ =(u2v3u3v2)i⃗ +(u3v1u1v3)j⃗ +(u1v2u2v1)k⃗ 

可以用以下行列式來簡記:
u⃗ ×v⃗ =i⃗ u1v1j⃗ u2v2k⃗ u3v3

如果叉乘的兩個向量都是平面向量,則可以看作是第三個分量爲0的三維向量。

以下Processing程序可以驗證叉乘用於點在直線哪一側的判斷的正確性:

PVector a = new PVector(100, 200);
PVector b = new PVector(300, 300);
PVector c = PVector.sub(b, a);

void setup() {
  size(400, 400);
  fill(0);
}

void draw() {
  background(255);
  line(a.x, a.y, b.x, b.y);
  PVector d = new PVector(mouseX - a.x, mouseY - a.y);

  String side;
  if (c.cross(d).z > 0)
    side = "left";
  else if (c.cross(d).z < 0)
    side = "right";
  else
    side = "on";
  text(side, 40, 40);
}

有興趣的讀者也可以把cross方法展開試試。

算法實現

現在算法已經很明顯啦!其中有一點小技巧,三角形的三個頂點是轉着來的,算一次就行了。比如,在上圖中,點C在直線AB左側,點B在直線CA的左側,點A在直接BC的左側。所以,第一步是先計算三角形的方向:

float signOfTrig = (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x);

注意這樣一下子寫出來不太容易看明白,但是如果看成向量AB和向量AC叉乘之後的Z座標就好懂的多了。

再分別計算P在AB、CA、BC的哪一側:

float signOfAB = (b.x - a.x)*(p.y - a.y) - (b.y - a.y)*(p.x - a.x);
float signOfCA = (a.x - c.x)*(p.y - c.y) - (a.y - c.y)*(p.x - c.x);
float signOfBC = (c.x - b.x)*(p.y - c.y) - (c.y - b.y)*(p.x - c.x);

最後判斷它們是否在同一側:

boolean d1 = (signOfAB * signOfTrig > 0);
boolean d2 = (signOfCA * signOfTrig > 0);
boolean d3 = (signOfBC * signOfTrig > 0);
println(d1 && d1 && d3);

這就是所有的算法了!最後來個程序驗證一下。

驗證程序

PVector[] trig;
float r = 150;
float t = 0;
float interval = 30;

void setup() {
  size(500, 500);
  trig = new PVector[3];
  ellipseMode(CENTER);
}

void draw() {
  translate(width/2, height/2);
  updateTrig();
  background(0);
  stroke(255);
  line(trig[0].x, trig[0].y, trig[1].x, trig[1].y);
  line(trig[1].x, trig[1].y, trig[2].x, trig[2].y);
  line(trig[0].x, trig[0].y, trig[2].x, trig[2].y);

  noStroke();
  for (float i = -width/2 + interval/2; i < width/2; i += interval) {
    for (float j = -height/2 + interval/2; j < height/2; j += interval) {
      if (inTrig(i, j)) {
        fill(255, 0, 0);
      } else {
        fill(255);
      }
      ellipse(i, j, 2, 2);
    }
  }
  t += 0.5;
}

void updateTrig() {
  for (int i = 0; i < 3; i++)
    trig[i] = new PVector(r * cos(radians(i * 120 + t)), r * sin(radians(i * 120 + t)));
}

boolean inTrig(float x, float y) {
  PVector a = trig[0];
  PVector b = trig[1];
  PVector c = trig[2];
  PVector p = new PVector(x, y);

  float signOfTrig = (b.x - a.x)*(c.y - a.y) - (b.y - a.y)*(c.x - a.x);
  float signOfAB = (b.x - a.x)*(p.y - a.y) - (b.y - a.y)*(p.x - a.x);
  float signOfCA = (a.x - c.x)*(p.y - c.y) - (a.y - c.y)*(p.x - c.x);
  float signOfBC = (c.x - b.x)*(p.y - c.y) - (c.y - b.y)*(p.x - c.x);

  boolean d1 = (signOfAB * signOfTrig > 0);
  boolean d2 = (signOfCA * signOfTrig > 0);
  boolean d3 = (signOfBC * signOfTrig > 0);

  return d1 && d2 && d3;
}

效果如下:

效果

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章