Я создал 3D-движок с нуля на JavaScript, используя однородные координаты и полный матричный конвейер MVP. Я переключился с углов Эйлера на кватернионы, чтобы решить проблему блокировки карданного подвеса, но столкнулся с новой проблемой: после отклонения камеры от курса и последующего применения наклона или крена камера создает раскачивающуюся фигуру в виде восьмерки вместо постоянного вращения. Реализация кватерниона:
Я представляю ориентацию в виде кватерниона (w, x, y, z) и накапливаю повороты, используя произведение Гамильтона, в каждом кадре. Я нормализую после каждого умножения, чтобы предотвратить дрейф длины. Что я пробовал:
Фиксированные оси мирового пространства — жесткое кодирование (1,0,0) для тангажа и (0,1,0) для отклонения от курса вместо локальных осей. Это устраняет раскачивание, но вновь вводит блокировку подвеса.
Извлечение локальной оси с помощью матрицы вращения — преобразование кватерниона в матрицу вращения 4x4 и извлечение локального правого вектора (столбец 0) для угла наклона и локального прямого вектора (столбец 2) для крена. Это должно дать правильное вращение в локальном пространстве, но приводит к колебанию в форме восьмерки после отклонения от курса.
Ортогонализация по Граму-Шмидту — ортогонализация извлеченных базисных векторов перед использованием их в качестве осей вращения, чтобы исправить любой дрейф с плавающей запятой в столбцах матрицы. Скалярное произведение между прямым и правым было ровно 0 после ортогонализации, поэтому оси чистые.
Исправление переворота кватерниона с двойным покрытием — проверка скалярного произведения между текущим кватернионом и новым кватернионом вращения перед умножением и отрицание, если оно отрицательное, чтобы обеспечить интерполяцию кратчайшего пути.
Я создал 3D-движок с нуля на JavaScript, используя однородные координаты и полный матричный конвейер MVP. Я переключился с углов Эйлера на кватернионы, чтобы решить проблему блокировки карданного подвеса, но столкнулся с новой проблемой: после отклонения камеры от курса и последующего применения наклона или крена камера создает раскачивающуюся фигуру в виде восьмерки вместо постоянного вращения. [b]Реализация кватерниона:[/b] Я представляю ориентацию в виде кватерниона (w, x, y, z) и накапливаю повороты, используя произведение Гамильтона, в каждом кадре. Я нормализую после каждого умножения, чтобы предотвратить дрейф длины. [b]Что я пробовал:[/b] [list] [*][b]Фиксированные оси мирового пространства[/b] — жесткое кодирование (1,0,0) для тангажа и (0,1,0) для отклонения от курса вместо локальных осей. Это устраняет раскачивание, но вновь вводит блокировку подвеса.
[*][b]Извлечение локальной оси с помощью матрицы вращения[/b] — преобразование кватерниона в матрицу вращения 4x4 и извлечение локального правого вектора (столбец 0) для угла наклона и локального прямого вектора (столбец 2) для крена. Это должно дать правильное вращение в локальном пространстве, но приводит к колебанию в форме восьмерки после отклонения от курса.
[*][b]Ортогонализация по Граму-Шмидту[/b] — ортогонализация извлеченных базисных векторов перед использованием их в качестве осей вращения, чтобы исправить любой дрейф с плавающей запятой в столбцах матрицы. Скалярное произведение между прямым и правым было ровно 0 после ортогонализации, поэтому оси чистые.
[*][b]Исправление переворота кватерниона с двойным покрытием[/b] — проверка скалярного произведения между текущим кватернионом и новым кватернионом вращения перед умножением и отрицание, если оно отрицательное, чтобы обеспечить интерполяцию кратчайшего пути.
class Camera { constructor (x, y, z){ this.x = x; this.y = y; this.z = z; this.rate = 10;
this.q = new Quaternion(1, 0, 0, 0);
this.yaw = 0; this.pitch = 0; this.roll = 0;
}
controls(){ const rotM = this.q.convertToM();
const forward = { x: rotM[0][2], y: rotM[1][2], z: rotM[2][2] }; //store as a vector instead of just a scalar let right = { x: rotM[0][0], y: rotM[1][0], z: rotM[2][0] }; let up = { x: rotM[0][1], y: rotM[1][1], z: rotM[2][1] };
//right = right - (right·forward / forward·forward) * forward //up = up - (up·forward / forward·forward) * forward - (up·right / right·right) * right
right = vectorSubtraction(right, vectorByNumber(vectorDotProduct(right, forward)/vectorDotProduct(forward, forward), forward))
up = vectorSubtraction(up, vectorByNumber(vectorDotProduct(up, forward)/vectorDotProduct(forward, forward), forward)); up = vectorSubtraction(up, vectorByNumber(vectorDotProduct(up, right)/vectorDotProduct(right, right), right));
let rightLength = Math.sqrt(right.x**2 + right.y**2 + right.z**2); let upLength = Math.sqrt(up.x**2 + up.y**2 + up.z**2);
right = vectorByNumber(1/rightLength, right); //normalize vectors up = vectorByNumber(1/upLength, up);
this.V = [ new Vertex(x + this.width, y + this.height, z - this.depth, w), new Vertex(x + this.width, y - this.height, z - this.depth, w), new Vertex(x - this.width, y - this.height, z - this.depth, w), new Vertex(x - this.width, y + this.height, z - this.depth, w), new Vertex(x + this.width, y + this.height, z + this.depth, w), new Vertex(x + this.width, y - this.height, z + this.depth, w), new Vertex(x - this.width, y - this.height, z + this.depth, w), new Vertex(x - this.width, y + this.height, z + this.depth, w) ]; } }
//used for combining rotations const multiplyMatMat = (a, b) => { let m = [[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,0]]; for (let r = 0; r < 4; r++) for (let c = 0; c < 4; c++) for (let k = 0; k < 4; k++) m[r][c] += a[r][k] * b[k][c]; return m; }
const cam = new Camera(0, 0, -1000); //, 0, 0, 10
const cubeSize = 200;
const cubes = [ // back wall new Cube(-600, 0, 400, 1, cubeSize, cubeSize, cubeSize), new Cube(-400, 0, 400, 1, cubeSize, cubeSize, cubeSize), new Cube(-200, 0, 400, 1, cubeSize, cubeSize, cubeSize), new Cube(0, 0, 400, 1, cubeSize, cubeSize, cubeSize), new Cube(200, 0, 400, 1, cubeSize, cubeSize, cubeSize), new Cube(400, 0, 400, 1, cubeSize, cubeSize, cubeSize), new Cube(600, 0, 400, 1, cubeSize, cubeSize, cubeSize),
// left wall new Cube(-600, 0, 200, 1, cubeSize, cubeSize, cubeSize), new Cube(-600, 0, 0, 1, cubeSize, cubeSize, cubeSize), new Cube(-600, 0,-200, 1, cubeSize, cubeSize, cubeSize), new Cube(-600, 0,-400, 1, cubeSize, cubeSize, cubeSize),
// right wall new Cube(600, 0, 200, 1, cubeSize, cubeSize, cubeSize), new Cube(600, 0, 0, 1, cubeSize, cubeSize, cubeSize), new Cube(600, 0,-200, 1, cubeSize, cubeSize, cubeSize), new Cube(600, 0,-400, 1, cubeSize, cubeSize, cubeSize), ];
const V = []; const M = [];
//add cubes to list for (const cube of cubes) { const offset = V.length; V.push(...cube.V); M.push(...cube.M.map(tri => tri.map(i => i + offset))); }
let angle = 0; //angle of rotation
const main = () => { let projectedMatrix = []; ctx.clearRect(0, 0, CW, CH); cam.controls();
let view = multiplyMatMat(cam.q.convertToM(), translateM( cam.x, cam.y, cam.z)); let final = multiplyMatMat(proj, view);
for (let p of V){ let result = multiplyMatVec(final, p); const screen = toScreen(result.x, result.y, result.z, result.w) // drawPoints(screen.x, screen.y); projectedMatrix.push(screen); }
for (let p of M){ const p1 = projectedMatrix[p[0]]; const p2 = projectedMatrix[p[1]]; const p3 = projectedMatrix[p[2]];