Параметричні поверхні в Three.js
Параметрична поверхня — це відображення з 2D-області — простору параметрів
(u, v) ∈ [0,1]² — у 3D-точку P(u,v). Ця проста ідея породжує
сфери, тори, гіперболоїди, пляшки Кляйна та будь-яку іншу гладку
поверхню, що використовується в 3D-графіці. Ми виводимо рівняння для кількох
класичних поверхонь, будуємо Three.js BufferGeometry з
нуля, обчислюємо аналітичні нормалі та додаємо затінення за картою нормалей для
деталізації поверхні.
1. Параметричні поверхні
Параметрична поверхня — це гладка функція P : ℝ² → ℝ³. Для кожної пари параметрів (u, v) в деякій області Ω ми отримуємо точку:
Поверхня регулярна в (u,v), якщо N ≠ 0. У сингулярних точках (де T_u і T_v паралельні) нормаль не визначена — полюси сфери є класичним прикладом.
2. Тор
Тор утворюється протягуванням кола радіуса r (радіус трубки) вздовж іншого кола радіуса R (великий радіус):
3. Гіперболоїд
Однопорожнинний гіперболоїд — це двічі лінійчата поверхня: через кожну точку проходять дві прямі, що цілком лежать на поверхні. Саме тому градирні будують у цій формі: прямі балки, вигнута форма.
Двопорожнинний гіперболоїд — це x²/a² + y²/b² − z²/c² = −1 — параметризований через sinh для x,y та cosh для z.
4. Пляшка Кляйна
Пляшка Кляйна — це неорієнтовна поверхня без краю — її неможливо вкласти в ℝ³ без самоперетину. Параметризація «занурення вісімкою»:
5. Нормалі вершин з частинних похідних
Для будь-якої параметричної поверхні нормалі можна обчислити аналітично скінченними різницями або точним символьним диференціюванням. Наближення скінченними різницями в параметрі (u, 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 обчислюється для кожного фрагмента за допомогою частинних похідних:
Three.js обчислює дотичні вектори автоматично з UV, якщо ви викличете
geometry.computeTangents() після встановлення атрибутів UV та
нормалей.
8. Більше поверхонь
Стрічка Мебіуса
x=(1+v/2·cos(u/2))·cos u, y=(1+v/2·cos(u/2))·sin u, z=v/2·sin(u/2). Неорієнтовна поверхня з 1D-краєм.
Поверхня Боя
Неорієнтовна, без самоперетину в ℝ³ на відміну від пляшки Кляйна. Побудована через відображення ℝP² в ℝ³, відкрите Вернером Боєм (1901).
Поверхня Еннепера
x=u−u³/3+uv², y=v−v³/3+vu², z=u²−v². Мінімальна поверхня (середня кривина H=0 усюди).
Мушля / гелікоїд
x=r(v)·cos(Nu)·cos(v), y=r(v)·cos(Nu)·sin(v), z=−r(v)·sin(Nu). Породжує форми мушель черевоногих, експоненційно змінюючи r(v).