Поворачиваем объекты

Мы продолжаем «издеваться» над объектом сцены и на этот раз добавляем повороты. Вращаться объект будет только в одной плоскости — плоскости XY. И первым делом мы рассмотрим математическую составляющую, которая будет немного сложнее, чем в переносе и масштабировании.

Итак, у нас есть только один параметр — это угол поворота. Для того, чтобы повернуть объект на angle градусов, нам понадобится сначала найти синус и косинус этого угла:

  • S = sin(angle)
  • C = cos(angle)

Тогда для вершины с координатами X и Y итоговая формула будем иметь следующий вид:

  • X’ = X * C + Y * S
  • Y’ = Y * C — X * S

… где S и C — синус и косинус угла, рассмотренные чуть выше. Математическое обоснование для этой формулы в целях простоты и краткости мы рассматривать не будем и примем формулу на веру.

Итак, формула у нас есть, приступим к реализации. Начнем с вершинного шейдера:


    attribute vec3 aVertexPosition;

    uniform vec2 uTranslation;
    uniform vec2 uScale;
    uniform float uAngle;

    void main(void) {
        float angleRadians = radians(uAngle);
        vec2 uRotation = vec2(sin(angleRadians), cos(angleRadians));
        vec2 rotated = vec2(
            aVertexPosition.x * uRotation.y + aVertexPosition.y * uRotation.x,
            aVertexPosition.y * uRotation.y - aVertexPosition.x * uRotation.x
        );
        vec3 scaled = vec3(rotated * uScale, aVertexPosition.z);
        gl_Position = vec4(scaled, 1.0) + vec4(uTranslation, 0.0, 0.0);
    }

Мы видим, что добавилась новая uniform-переменная uAngle типа float — это угол поворота объекта. Далее тело шейдера в точности отражает описанную выше формулу. Единственное, что не относится напрямую к формуле, — предварительное преобразование градусов (которые нам придут из скриптов) в радианы.

Что ж, не так сложно, как казалось. Идем в JavaScript. И начнем мы с объявления глобальных переменных, где добавилась переменная rotation для хранения угла поворота и новый тип события rotate:


    var translation = [0, 0];
    var scale = [1, 1];
    var rotation = 0;
    
    var currentEvent;
    var eventType = {
        drag: 1,
        scale: 2,
        rotate: 3
    };

Далее взглянем на функцию обработки нажатия мыши:


    function onMouseDown(evt){
        if (evt.shiftKey) {
            currentEvent = eventType.scale;
        } else if (evt.ctrlKey) {
            currentEvent = eventType.rotate;
        } else {
            currentEvent = eventType.drag;
        }
        dragStartX = evt.clientX;
        dragStartY = evt.clientY;
        dragOffset = [0, 0];
        mousePressed = true;
    }

То есть событие поворота начинается с нажатия кнопки мыши при зажатой клавише Ctrl. Затем идет функция обработки самого поворота (когда мы перемещаем мышь при зажатой кнопке):


    function onMouseMove(evt) {
        if (mousePressed) {
            var diffX = evt.clientX - dragStartX;
            var diffY = dragStartY - evt.clientY;
            if (currentEvent === eventType.drag) {
                dragOffset = [diffX * 2 / 500, diffY * 2 / 500];
                var finalTranslation = [translation[0] + dragOffset[0], translation[1] + dragOffset[1]]
                gl.uniform2fv(shaderProgram.translationUniform, finalTranslation);
            } else if (currentEvent === eventType.scale) {
                dragOffset = [diffX / 100, diffY / 100];
                var finalScale = [scale[0] + dragOffset[0], scale[1] + dragOffset[1]]
                gl.uniform2fv(shaderProgram.scaleUniform, finalScale);
            } else if (currentEvent === eventType.rotate) {
                dragOffset = diffX;
                gl.uniform1f(shaderProgram.angleUniform, rotation + dragOffset);
            }
        }
    }

Первое, на что стоит обратить внимание, — dragOffset хранит не массив, как в случае с переносом и масштабированием, а одно значение. (Вообще, это плохая практика, когда в одной переменной хранятся разные типы, но так как у нас «песочница», закроем на это глаза.) Смысл такого изменения вполне понятен — нам нужно значение только одного угла для вращения в одной плоскости. Затем мы передаем это значение напрямую в шейдер без каких-либо изменений. Это означает, что угол поворота объекта будет равен количеству пройденных мышью пикселей (расстояние считается только при движении мыши по горизонтали).

И, наконец, функция отпускания кнопки мыши:


    function onMouseUp(evt){
        if (mousePressed) {
            if (currentEvent === eventType.drag) {
                translation = [translation[0] + dragOffset[0], translation[1] + dragOffset[1]];
            } else if (currentEvent === eventType.scale) {
                scale = [scale[0] + dragOffset[0], scale[1] + dragOffset[1]];
            } else if (currentEvent === eventType.rotate) {
                rotation = rotation + dragOffset;
            }
            mousePressed = false;
        }
    }

Вот и все! Теперь можно взглянуть на перевернутый и растянутый объект (слева оригинал):

Мы видим, что буква вращается вокруг своего центра. И это кажется правильным. Но, признаться, это получилось случайно, а не планировалось заранее :). На самом деле все зависит от координат объекта. В нашем случае случилось так, что центр объекта совпал с нулевыми координатами. Стоит немного поднять объект в область положительных координат — и он начнет кружиться вокруг своего левого нижнего края (примерно). Чтобы не менять все координаты объекта, нам достаточно немного изменить шейдер, чтобы получить нужный эффект (скачайте исходный код, чтобы поэкспериментировать):


    attribute vec3 aVertexPosition;

    uniform vec2 uTranslation;
    uniform vec2 uScale;
    uniform float uAngle;

    void main(void) {
        vec3 newVertexPosition = vec3(aVertexPosition.x + 0.5, aVertexPosition.y + 0.5, aVertexPosition.z);
        float angleRadians = radians(uAngle);
        vec2 uRotation = vec2(sin(angleRadians), cos(angleRadians));
        vec2 rotated = vec2(
            newVertexPosition.x * uRotation.y + newVertexPosition.y * uRotation.x,
            newVertexPosition.y * uRotation.y - newVertexPosition.x * uRotation.x
        );
        vec3 scaled = vec3(rotated * uScale, newVertexPosition.z);
        gl_Position = vec4(scaled, 1.0) + vec4(uTranslation, 0.0, 0.0);
    }

Вы видите, как смещение объекта в начале шейдера влияет на поведение. Именно поэтому важно соблюдать порядок применения трансформаций: сначала поворот, затем масштабирование, затем перенос. Попробуйте изменить порядок трансформаций и посмотреть, к чему это приведет.

Ну вот, теперь можно подводить итоги. Пока у нас вполне получается обходиться без матриц. Относительно небольшими усилиями можно дополнить логику для работы с координатой Z — и у нас будет все, что нужно. Так почему же все работают через матрицы? Разберемся в этом в следующий раз.

Поворачиваем объекты
Метки:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *