Базова геометрія
У цій статті ми розглянемо базові операції над точками в евклідовому просторі, які лежать в основі всієї аналітичної геометрії. Для кожної точки ми розглядатимемо вектор , спрямований із у . Надалі ми не розрізнятимемо і та вживатимемо термін точка як синонім до вектора.
Лінійні операції
Як двовимірні, так і тривимірні точки утворюють лінійний простір, а це означає, що для них визначені сума точок та множення точки на деяке число. Ось ці базові реалізації для 2D:
- C++
- Python
- TypeScript
- Go
struct point2d {
ftype x, y;
point2d() {}
point2d(ftype x, ftype y): x(x), y(y) {}
point2d& operator+=(const point2d &t) {
x += t.x;
y += t.y;
return *this;
}
point2d& operator-=(const point2d &t) {
x -= t.x;
y -= t.y;
return *this;
}
point2d& operator*=(ftype t) {
x *= t;
y *= t;
return *this;
}
point2d& operator/=(ftype t) {
x /= t;
y /= t;
return *this;
}
point2d operator+(const point2d &t) const {
return point2d(*this) += t;
}
point2d operator-(const point2d &t) const {
return point2d(*this) -= t;
}
point2d operator*(ftype t) const {
return point2d(*this) *= t;
}
point2d operator/(ftype t) const {
return point2d(*this) /= t;
}
};
point2d operator*(ftype a, point2d b) {
return b * a;
}
from dataclasses import dataclass
# ftype — це тип координат (float або int).
# У Python зручно перевантажувати оператори, що є прямим
# відповідником C++-перевантаженням +, -, *, /.
@dataclass
class Point2d:
x: float
y: float
def __add__(self, t: "Point2d") -> "Point2d":
return Point2d(self.x + t.x, self.y + t.y)
def __sub__(self, t: "Point2d") -> "Point2d":
return Point2d(self.x - t.x, self.y - t.y)
def __mul__(self, t: float) -> "Point2d":
return Point2d(self.x * t, self.y * t)
# дозволяє писати a * point — симетрично до C++ operator*(ftype, point2d)
def __rmul__(self, t: float) -> "Point2d":
return self * t
def __truediv__(self, t: float) -> "Point2d":
return Point2d(self.x / t, self.y / t)
// У TypeScript немає перевантаження операторів, тому
// лінійні операції реалізуємо явними методами add/sub/mul/div.
class Point2d {
constructor(public x: number, public y: number) {}
add(t: Point2d): Point2d {
return new Point2d(this.x + t.x, this.y + t.y);
}
sub(t: Point2d): Point2d {
return new Point2d(this.x - t.x, this.y - t.y);
}
mul(t: number): Point2d {
return new Point2d(this.x * t, this.y * t);
}
div(t: number): Point2d {
return new Point2d(this.x / t, this.y / t);
}
}
// У Go теж немає перевантаження операторів — лінійні операції
// реалізуємо методами на struct, що повертають нову точку.
type Point2d struct {
X, Y float64
}
func (a Point2d) Add(t Point2d) Point2d {
return Point2d{a.X + t.X, a.Y + t.Y}
}
func (a Point2d) Sub(t Point2d) Point2d {
return Point2d{a.X - t.X, a.Y - t.Y}
}
func (a Point2d) Mul(t float64) Point2d {
return Point2d{a.X * t, a.Y * t}
}
func (a Point2d) Div(t float64) Point2d {
return Point2d{a.X / t, a.Y / t}
}
А також для 3D точок:
- C++
- Python
- TypeScript
- Go
struct point3d {
ftype x, y, z;
point3d() {}
point3d(ftype x, ftype y, ftype z): x(x), y(y), z(z) {}
point3d& operator+=(const point3d &t) {
x += t.x;
y += t.y;
z += t.z;
return *this;
}
point3d& operator-=(const point3d &t) {
x -= t.x;
y -= t.y;
z -= t.z;
return *this;
}
point3d& operator*=(ftype t) {
x *= t;
y *= t;
z *= t;
return *this;
}
point3d& operator/=(ftype t) {
x /= t;
y /= t;
z /= t;
return *this;
}
point3d operator+(const point3d &t) const {
return point3d(*this) += t;
}
point3d operator-(const point3d &t) const {
return point3d(*this) -= t;
}
point3d operator*(ftype t) const {
return point3d(*this) *= t;
}
point3d operator/(ftype t) const {
return point3d(*this) /= t;
}
};
point3d operator*(ftype a, point3d b) {
return b * a;
}
@dataclass
class Point3d:
x: float
y: float
z: float
def __add__(self, t: "Point3d") -> "Point3d":
return Point3d(self.x + t.x, self.y + t.y, self.z + t.z)
def __sub__(self, t: "Point3d") -> "Point3d":
return Point3d(self.x - t.x, self.y - t.y, self.z - t.z)
def __mul__(self, t: float) -> "Point3d":
return Point3d(self.x * t, self.y * t, self.z * t)
def __rmul__(self, t: float) -> "Point3d":
return self * t
def __truediv__(self, t: float) -> "Point3d":
return Point3d(self.x / t, self.y / t, self.z / t)
class Point3d {
constructor(public x: number, public y: number, public z: number) {}
add(t: Point3d): Point3d {
return new Point3d(this.x + t.x, this.y + t.y, this.z + t.z);
}
sub(t: Point3d): Point3d {
return new Point3d(this.x - t.x, this.y - t.y, this.z - t.z);
}
mul(t: number): Point3d {
return new Point3d(this.x * t, this.y * t, this.z * t);
}
div(t: number): Point3d {
return new Point3d(this.x / t, this.y / t, this.z / t);
}
}
type Point3d struct {
X, Y, Z float64
}
func (a Point3d) Add(t Point3d) Point3d {
return Point3d{a.X + t.X, a.Y + t.Y, a.Z + t.Z}
}
func (a Point3d) Sub(t Point3d) Point3d {
return Point3d{a.X - t.X, a.Y - t.Y, a.Z - t.Z}
}
func (a Point3d) Mul(t float64) Point3d {
return Point3d{a.X * t, a.Y * t, a.Z * t}
}
func (a Point3d) Div(t float64) Point3d {
return Point3d{a.X / t, a.Y / t, a.Z / t}
}
Тут ftype — це деякий тип, що використовується для координат, зазвичай int, double або long long.
Скалярний добуток
Означення
Скалярний добуток векторів та можна означити двома еквівалентними способами. Геометрично це добуток довжини першого вектора на довжину проєкції другого вектора на перший. Як видно із зображення нижче, ця проєкція — це не що інше, як , де — кут між та . Отже, .
Скалярний добуток має кілька важливих властивостей:
Тобто це комутативна функція, лінійна за обома аргументами. Позначимо одиничні вектори як
З цим позначенням ми можемо записати вектор як . А оскільки для одиничних векторів
ми бачимо, що в координатах для та виконується
Це також є алгебраїчним означенням скалярного добутку. Звідси ми можемо записати функції, які його обчислюють.
- C++
- Python
- TypeScript
- Go
ftype dot(point2d a, point2d b) {
return a.x * b.x + a.y * b.y;
}
ftype dot(point3d a, point3d b) {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
def dot2d(a: Point2d, b: Point2d) -> float:
return a.x * b.x + a.y * b.y
def dot3d(a: Point3d, b: Point3d) -> float:
return a.x * b.x + a.y * b.y + a.z * b.z
// У TypeScript нема перевантаження за типом аргументів,
// тому даємо двом версіям різні імена (dot2d / dot3d).
function dot2d(a: Point2d, b: Point2d): number {
return a.x * b.x + a.y * b.y;
}
function dot3d(a: Point3d, b: Point3d): number {
return a.x * b.x + a.y * b.y + a.z * b.z;
}
// Go не має перевантаження функцій, тому окремі імена для 2D/3D.
func Dot2d(a, b Point2d) float64 {
return a.X*b.X + a.Y*b.Y
}
func Dot3d(a, b Point3d) float64 {
return a.X*b.X + a.Y*b.Y + a.Z*b.Z
}
Розв'язуючи задачі, для обчислення скалярних добутків варто користуватися алгебраїчним означенням, але тримати в голові геометричне означення та властивості, щоб ним користуватися.
Властивості
За допомогою скалярного добутку ми можемо визначити багато геометричних властивостей. Наприклад
- Норма (квадрат довжини):
- Довжина :
- Проєкція на :
- Кут між векторами:
- З попереднього пункту видно, що скалярний добуток додатний, якщо кут між векторами гострий, від'ємний, якщо кут тупий, і дорівнює нулю, якщо вектори ортогональні, тобто утворюють прямий кут.
Зауважимо, що всі ці функції не залежать від кількості вимірів, а отже, вони будуть однаковими для випадків 2D та 3D:
- C++
- Python
- TypeScript
- Go
ftype norm(point2d a) {
return dot(a, a);
}
double abs(point2d a) {
return sqrt(norm(a));
}
double proj(point2d a, point2d b) {
return dot(a, b) / abs(b);
}
double angle(point2d a, point2d b) {
return acos(dot(a, b) / abs(a) / abs(b));
}
import math
def norm(a: Point2d) -> float:
return dot2d(a, a)
def abs_(a: Point2d) -> float: # abs — вбудована назва, тому abs_
return math.sqrt(norm(a))
def proj(a: Point2d, b: Point2d) -> float:
return dot2d(a, b) / abs_(b)
def angle(a: Point2d, b: Point2d) -> float:
return math.acos(dot2d(a, b) / abs_(a) / abs_(b))
function norm(a: Point2d): number {
return dot2d(a, a);
}
function abs(a: Point2d): number {
return Math.sqrt(norm(a));
}
function proj(a: Point2d, b: Point2d): number {
return dot2d(a, b) / abs(b);
}
function angle(a: Point2d, b: Point2d): number {
return Math.acos(dot2d(a, b) / abs(a) / abs(b));
}
import "math"
func Norm(a Point2d) float64 {
return Dot2d(a, a)
}
func Abs(a Point2d) float64 {
return math.Sqrt(Norm(a))
}
func Proj(a, b Point2d) float64 {
return Dot2d(a, b) / Abs(b)
}
func Angle(a, b Point2d) float64 {
return math.Acos(Dot2d(a, b) / Abs(a) / Abs(b))
}
Щоб побачити наступну важливу властивість, розгляньмо множину точок , для яких для деякої фіксованої сталої . Можна побачити, що ця множина точок — це саме множина точок, для яких проєкція на є точкою , і вони утворюють гіперплощину, ортогональну до . На зображенні нижче ви можете побачити вектор разом із кількома такими векторами, що мають із ним однаковий скалярний добуток у 2D:

У 2D ці вектори утворять пряму, у 3D — площину. Зауважимо, що цей результат дозволяє нам означити пряму в 2D як або , де — вектор, ортогональний до прямої, — будь-який вектор, що вже лежить на прямій, а . Так само можна означити й площину в 3D.
Векторний добуток
Означення
Припустимо, у вас є три вектори , та у тривимірному просторі, об'єднані в паралелепіпед, як на зображенні нижче:
Як би ви обчислили його об'єм? Зі школи ми знаємо, що треба помножити площу основи на висоту, якою є проєкція на напрямок, ортогональний до основи. Це означає, що якщо ми означимо як вектор, ортогональний одночасно до та , довжина якого дорівнює площі паралелограма, утвореного та , то дорівнюватиме об'єму паралелепіпеда. Для цілісності будемо вважати, що завжди спрямований так, щоб поворот від вектора до вектора , якщо дивитися з боку , завжди був проти годинникової стрілки (див. зображення нижче).
Це означує векторний добуток векторів та , а також мішаний добуток векторів , та .
Деякі важливі властивості векторного та мішаного добутків:
-
-
-
Для будь-яких та існує рівно один вектор такий, що для будь-якого вектора .
Справді, якщо існують два таких вектори та , то для всіх векторів , що можливо лише тоді, коли . -
-
. Справді, для всіх векторів виконується ланцюжок рівностей:
Що доводить завдяки пункту 3.
-
, де — кут між та , оскільки дорівнює площі паралелограма, утвореного та .
З урахуванням усього цього та того, що для одиничних векторів виконується рівність
ми можемо обчислити векторний добуток та у координатній формі:
Що також можна записати в елегантнішій формі:
Тут позначає визначник матриці.
Деякий аналог векторного добутку (а саме псевдоскалярний добуток) можна реалізувати й у двовимірному випадку. Якщо ми хочемо обчислити площу паралелограма, утвореного векторами та , ми обчислили б . Інший спосіб отримати той самий результат — помножити (основу паралелограма) на висоту, якою є проєкція вектора на вектор , повернутий на , що, своєю чергою, дорівнює . Тобто обчислити .
Якщо враховувати знак, то площа буде додатною, якщо поворот від до (тобто з боку точки ) відбувається проти годинникової стрілки, і від'ємною інакше. Це означує псевдоскалярний добуток. Зауважимо, що він також дорівнює , де — кут від до , відлічуваний проти годинникової стрілки (і від'ємний, якщо поворот за годинниковою стрілкою).
Реалізуймо все це!
- C++
- Python
- TypeScript
- Go
point3d cross(point3d a, point3d b) {
return point3d(a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x);
}
ftype triple(point3d a, point3d b, point3d c) {
return dot(a, cross(b, c));
}
ftype cross(point2d a, point2d b) {
return a.x * b.y - a.y * b.x;
}
def cross3d(a: Point3d, b: Point3d) -> Point3d:
return Point3d(a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x)
def triple(a: Point3d, b: Point3d, c: Point3d) -> float:
return dot3d(a, cross3d(b, c))
# у 2D векторний добуток — це скаляр (псевдоскалярний добуток)
def cross2d(a: Point2d, b: Point2d) -> float:
return a.x * b.y - a.y * b.x
function cross3d(a: Point3d, b: Point3d): Point3d {
return new Point3d(
a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x,
);
}
function triple(a: Point3d, b: Point3d, c: Point3d): number {
return dot3d(a, cross3d(b, c));
}
// у 2D повертаємо скаляр (псевдоскалярний добуток)
function cross2d(a: Point2d, b: Point2d): number {
return a.x * b.y - a.y * b.x;
}
func Cross3d(a, b Point3d) Point3d {
return Point3d{
a.Y*b.Z - a.Z*b.Y,
a.Z*b.X - a.X*b.Z,
a.X*b.Y - a.Y*b.X,
}
}
func Triple(a, b, c Point3d) float64 {
return Dot3d(a, Cross3d(b, c))
}
// у 2D повертаємо скаляр (псевдоскалярний добуток)
func Cross2d(a, b Point2d) float64 {
return a.X*b.Y - a.Y*b.X
}
Властивості
Щодо векторного добутку, то він дорівнює нульовому вектору тоді й лише тоді, коли вектори та колінеарні (лежать на одній прямій, тобто паралельні). Те саме виконується для мішаного добутку: він дорівнює нулю тоді й лише тоді, коли вектори , та компланарні (лежать в одній площині).
Звідси ми можемо отримати універсальні рівняння, що задають прямі та площини. Пряму можна задати через її напрямний вектор та початкову точку або через дві точки та . Вона задається як або як . Щодо площин, то площину можна задати трьома точками , та як або початковою точкою та двома напрямними векторами, що лежать у цій площині, та : .
У 2D псевдоскалярний добуток також можна використати, щоб перевірити взаємну орієнтацію двох векторів, бо він додатний, якщо поворот від першого до другого вектора відбувається проти годинникової стрілки, і від'ємний інакше. І, звісно, його можна використовувати для обчислення площ многокутників, що описано в окремій статті. Мішаний добуток можна використати з тією самою метою в тривимірному просторі.
Вправи
Перетин прямих
Існує багато способів задати пряму в 2D, і не варто вагатися їх комбінувати. Наприклад, у нас є дві прямі, і ми хочемо знайти точки їхнього перетину. Можна сказати, що всі точки першої прямої можна параметризувати як , де — початкова точка, — напрямок, а — деякий дійсний параметр. Щодо другої прямої, то всі її точки мають задовольняти . Звідси ми легко знаходимо параметр :
Реалізуймо функцію перетину двох прямих.
- C++
- Python
- TypeScript
- Go
point2d intersect(point2d a1, point2d d1, point2d a2, point2d d2) {
return a1 + cross(a2 - a1, d2) / cross(d1, d2) * d1;
}
def intersect_lines(a1: Point2d, d1: Point2d,
a2: Point2d, d2: Point2d) -> Point2d:
t = cross2d(a2 - a1, d2) / cross2d(d1, d2)
return a1 + d1 * t
function intersectLines(
a1: Point2d, d1: Point2d,
a2: Point2d, d2: Point2d,
): Point2d {
const t = cross2d(a2.sub(a1), d2) / cross2d(d1, d2);
return a1.add(d1.mul(t));
}
func IntersectLines(a1, d1, a2, d2 Point2d) Point2d {
t := Cross2d(a2.Sub(a1), d2) / Cross2d(d1, d2)
return a1.Add(d1.Mul(t))
}
Перетин площин
Однак іноді скористатися геометричними міркуваннями буває важко. Наприклад, вам задано три площини через початкові точки та напрямки , і ви хочете знайти точку їхнього перетину. Можна помітити, що достатньо просто розв'язати систему рівнянь:
Замість того щоб думати над геометричним підходом, можна виробити алгебраїчний, який отримуємо одразу. Наприклад, якщо ви вже реалізували клас точки, вам буде легко розв'язати цю систему за допомогою правила Крамера, бо мішаний добуток — це просто визначник матриці, отриманої з векторів, що є її стовпцями:
- C++
- Python
- TypeScript
- Go
point3d intersect(point3d a1, point3d n1, point3d a2, point3d n2, point3d a3, point3d n3) {
point3d x(n1.x, n2.x, n3.x);
point3d y(n1.y, n2.y, n3.y);
point3d z(n1.z, n2.z, n3.z);
point3d d(dot(a1, n1), dot(a2, n2), dot(a3, n3));
return point3d(triple(d, y, z),
triple(x, d, z),
triple(x, y, d)) / triple(n1, n2, n3);
}
def intersect_planes(a1: Point3d, n1: Point3d,
a2: Point3d, n2: Point3d,
a3: Point3d, n3: Point3d) -> Point3d:
x = Point3d(n1.x, n2.x, n3.x)
y = Point3d(n1.y, n2.y, n3.y)
z = Point3d(n1.z, n2.z, n3.z)
d = Point3d(dot3d(a1, n1), dot3d(a2, n2), dot3d(a3, n3))
return Point3d(triple(d, y, z),
triple(x, d, z),
triple(x, y, d)) / triple(n1, n2, n3)
function intersectPlanes(
a1: Point3d, n1: Point3d,
a2: Point3d, n2: Point3d,
a3: Point3d, n3: Point3d,
): Point3d {
const x = new Point3d(n1.x, n2.x, n3.x);
const y = new Point3d(n1.y, n2.y, n3.y);
const z = new Point3d(n1.z, n2.z, n3.z);
const d = new Point3d(dot3d(a1, n1), dot3d(a2, n2), dot3d(a3, n3));
return new Point3d(
triple(d, y, z),
triple(x, d, z),
triple(x, y, d),
).div(triple(n1, n2, n3));
}
func IntersectPlanes(a1, n1, a2, n2, a3, n3 Point3d) Point3d {
x := Point3d{n1.X, n2.X, n3.X}
y := Point3d{n1.Y, n2.Y, n3.Y}
z := Point3d{n1.Z, n2.Z, n3.Z}
d := Point3d{Dot3d(a1, n1), Dot3d(a2, n2), Dot3d(a3, n3)}
return Point3d{
Triple(d, y, z),
Triple(x, d, z),
Triple(x, y, d),
}.Div(Triple(n1, n2, n3))
}
Тепер ви можете спробувати самостійно знайти підходи до поширених геометричних операцій, щоб звикнути до всього цього.