Стаття
Геометрія · ⏱ ≈ 14 хв читання · Останнє оновлення: 23 червня 2026 р.

Параметричні поверхні в Three.js

Параметрична поверхня — це відображення з 2D-області — простору параметрів (u, v) ∈ [0,1]² — у 3D-точку P(u,v). Ця проста ідея породжує сфери, тори, гіперболоїди, пляшки Кляйна та будь-яку іншу гладку поверхню, що використовується в 3D-графіці. Ми виводимо рівняння для кількох класичних поверхонь, будуємо Three.js BufferGeometry з нуля, обчислюємо аналітичні нормалі та додаємо затінення за картою нормалей для деталізації поверхні.

1. Параметричні поверхні

Параметрична поверхня — це гладка функція P : ℝ² → ℝ³. Для кожної пари параметрів (u, v) в деякій області Ω ми отримуємо точку:

P(u, v) = (x(u,v), y(u,v), z(u,v)) Два дотичні вектори в P: T_u = ∂P/∂u = (∂x/∂u, ∂y/∂u, ∂z/∂u) T_v = ∂P/∂v = (∂x/∂v, ∂y/∂v, ∂z/∂v) Нормаль поверхні (назовні): N = T_u × T_v (векторний добуток, потім нормалізувати)

Поверхня регулярна в (u,v), якщо N ≠ 0. У сингулярних точках (де T_u і T_v паралельні) нормаль не визначена — полюси сфери є класичним прикладом.

2. Тор

Тор утворюється протягуванням кола радіуса r (радіус трубки) вздовж іншого кола радіуса R (великий радіус):

x(u,v) = (R + r·cos v) · cos u y(u,v) = (R + r·cos v) · sin u z(u,v) = r · sin v u ∈ [0, 2π) (довгота — навколо великого кільця) v ∈ [0, 2π) (широта — навколо трубки) Аналітична нормаль: T_u = (−(R+r·cos v)·sin u, (R+r·cos v)·cos u, 0) T_v = (−r·sin v·cos u, −r·sin v·sin u, r·cos v) N = T_u × T_v (потім нормалізувати) Окремі випадки: R = r → ріжкоподібний тор (внутрішнє коло стягується в точку) R < r → веретеноподібний тор (самоперетинний) R >> r → тонке кільце

3. Гіперболоїд

Однопорожнинний гіперболоїд — це двічі лінійчата поверхня: через кожну точку проходять дві прямі, що цілком лежать на поверхні. Саме тому градирні будують у цій формі: прямі балки, вигнута форма.

x(u,v) = a · cosh v · cos u y(u,v) = b · cosh v · sin u z(u,v) = c · sinh v u ∈ [0, 2π), v ∈ [−vmax, vmax] Неявна форма: x²/a² + y²/b² − z²/c² = 1 За a=b=1, c=1: гіперболоїд обертання (профіль градирні) Радіус талії: a (при v=0, cosh(0)=1, sinh(0)=0)

Двопорожнинний гіперболоїд — це x²/a² + y²/b² − z²/c² = −1 — параметризований через sinh для x,y та cosh для z.

4. Пляшка Кляйна

Пляшка Кляйна — це неорієнтовна поверхня без краю — її неможливо вкласти в ℝ³ без самоперетину. Параметризація «занурення вісімкою»:

Для u ∈ [0, π), v ∈ [0, 2π): x = (a + b·cos(u/2)·sin v − b·sin(u/2)·sin(2v)) · cos u y = (a + b·cos(u/2)·sin v − b·sin(u/2)·sin(2v)) · sin u z = b·sin(u/2)·sin v + b·cos(u/2)·sin(2v) Тут a контролює загальний розмір, b контролює радіус трубки. Самоперетин (у ℝ³) не є геометричним — це артефакт вкладення 4D-об'єкта в 3D.

5. Нормалі вершин з частинних похідних

Для будь-якої параметричної поверхні нормалі можна обчислити аналітично скінченними різницями або точним символьним диференціюванням. Наближення скінченними різницями в параметрі (u, v) з кроком δ:

T_u ≈ [P(u+δ,v) − P(u−δ,v)] / (2δ) T_v ≈ [P(u,v+δ) − P(u,v−δ)] / (2δ) N = normalise(T_u × T_v)

Для гладкого затінення Three.js також надає computeVertexNormals(), який усереднює нормалі граней у спільних вершинах. Аналітичні нормалі завжди кращі на полюсах та сингулярностях, де скінченні різниці втрачають точність.

6. Побудова BufferGeometry у Three.js

// Узагальнена параметрична поверхня → Three.js BufferGeometry
function buildParametricGeometry(fn, uSegs = 64, vSegs = 64) {
  const positions = [];
  const normals   = [];
  const uvs       = [];
  const indices   = [];
  const δ = 1e-4;

  for (let vi = 0; vi <= vSegs; vi++) {
    for (let ui = 0; ui <= uSegs; ui++) {
      const u = ui / uSegs;
      const v = vi / vSegs;

      const p  = fn(u, v);
      const pu = fn(u + δ, v);
      const pv = fn(u, v + δ);

      positions.push(p.x, p.y, p.z);
      uvs.push(u, v);

      // Дотичні вектори через скінченні різниці
      const tu = {x:(pu.x-p.x)/δ, y:(pu.y-p.y)/δ, z:(pu.z-p.z)/δ};
      const tv = {x:(pv.x-p.x)/δ, y:(pv.y-p.y)/δ, z:(pv.z-p.z)/δ};
      // Векторний добуток
      const nx = tu.y*tv.z - tu.z*tv.y;
      const ny = tu.z*tv.x - tu.x*tv.z;
      const nz = tu.x*tv.y - tu.y*tv.x;
      const nl = Math.sqrt(nx*nx + ny*ny + nz*nz) || 1;
      normals.push(nx/nl, ny/nl, nz/nl);
    }
  }

  // Побудова індексного буфера трикутників
  const stride = uSegs + 1;
  for (let vi = 0; vi < vSegs; vi++) {
    for (let ui = 0; ui < uSegs; ui++) {
      const a = vi * stride + ui;
      const b = a + stride;
      indices.push(a, b, a+1,  b, b+1, a+1);
    }
  }

  const geo = new THREE.BufferGeometry();
  geo.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
  geo.setAttribute('normal',   new THREE.Float32BufferAttribute(normals,   3));
  geo.setAttribute('uv',       new THREE.Float32BufferAttribute(uvs,       2));
  geo.setIndex(indices);
  return geo;
}

// Тор: R=1, r=0.4
const R = 1, r = 0.4;
const torusGeo = buildParametricGeometry((u, v) => {
  const [U, V] = [u * Math.PI * 2, v * Math.PI * 2];
  return {
    x: (R + r * Math.cos(V)) * Math.cos(U),
    y: (R + r * Math.cos(V)) * Math.sin(U),
    z: r * Math.sin(V)
  };
});

// Гіперболоїд: a=b=c=1, v ∈ [−1.5, 1.5]
const hyperGeo = buildParametricGeometry((u, v) => {
  const U = u * Math.PI * 2;
  const V = (v - 0.5) * 3;  // переотобразити на [−1.5, 1.5]
  return { x: Math.cosh(V) * Math.cos(U), y: Math.cosh(V) * Math.sin(U), z: Math.sinh(V) };
});

7. Нормал-мепінг

Нормал-мепінг додає ілюзію дрібної деталізації поверхні без додаткової геометрії. Карта нормалей — це текстура, що кодує нормалі поверхні для кожного текселя в дотичному просторі (RGB → xyz). Фрагментний шейдер перетворює ці нормалі у світовий простір за допомогою матриці TBN і використовує їх в обчисленні освітлення:

Матриця TBN у точці поверхні: T = дотична (вздовж напрямку u, у світовому просторі) B = бідотична (T × N) N = геометрична нормаль Збурена нормаль: n_tangent = normalMap.sample(uv) * 2 − 1 (розпакувати з [0,1]) n_world = normalise(TBN · n_tangent) У Three.js: MeshStandardMaterial або MeshPhongMaterial mat.normalMap = new THREE.TextureLoader().load(url) mat.normalScale = new THREE.Vector2(1, 1)

Матриця TBN обчислюється для кожного фрагмента за допомогою частинних похідних: Three.js обчислює дотичні вектори автоматично з UV, якщо ви викличете geometry.computeTangents() після встановлення атрибутів UV та нормалей.