Строим 3D проекты – Шаг 10

Для удобства изучения свойств нашего 3D объекта добавим интерактивное управление для поворота в любую сторону с любой скоростью, приближения/удаления, включения/отключения освещения как и текстуры.
Интерфейс позволяет управлять моделью и контролировать ее текущее положение. Для стандартных компьютеров удобно управлять с помощью клавиатуры и мыши, поэтому добавим такое управление тоже.
Сначала рассмотрим готовый рабочий вариант программы по ссылке здесь, а затем рассмотрим как мы преобразовали программу предыдущего шага для получения такого результата:

Интерфейс (кнопки)
Начнем с интерфейса пользователя, доступного на любом устройстве. Будем использовать нарисованные стандартные кнопки, ползунки, флажки, текстовые поля HTML.
Для адаптивности интерфейса к мобильным устройствам используем библиотеку Bootstap. Подключаем в html-файле бутстрап, jQuery и файлы для кнопок, клавиатуры и мыши. А в теле программы размещаем шаблон для вывода 3-ёх колонок:
<script src="js_std/jquery.min.js"></script> <link rel="stylesheet" href= "js_std/bootstrap/css/bootstrap.min.css"> <script src= "js_std/bootstrap/js/bootstrap.min.js"></script> <script src="js/controlas-button10pr.js"></script> <script src="js/controlas-keyboard10pr.js"></script> <script src="js/controlas-mouse10pr.js"></script> </head> <body > <div class="container-as"> <div class="container-fluid"> <div class="row"> <div class="col-xs-12 col-sm-8 col-md-5 col-lg-5"> <h2>Viewer</h2> <label id="errorMessage76canvas10pr" style="color: red;"> </label> <canvas id="my-canvas10pr" class="canvas-adaptive" width="600" height="600"> Error from Html: Your browser does not support the HTML5 canvas element. </canvas> </div> <div class="col-xs-12 col-sm-4 col-md-3 col-lg-3"> <h2>Controller</h2> </div> <div class="col-xs-12 col-sm-12 col-md-4 col-lg-4"> <h2>Help</h2> </div> </div> </div> </div>
Из шаблона видно:
Три колонки Help, Viewer, Controller в какой-то мере олицетворяют разделение как в шаблоне MVC.
Напишем в 3-ем столбце интерфейса информацию о модели реагирования на кнопки в виде помощи Help:

Теперь подготовим интерфейс контроллера соответственно Help:

Например, для создания элементов вращения относительно оси Х, используем следующий код:
<h2>Controller</h2> <div class="windowbox"> <h3>Rotate</h3> <input class='handonbutton' type='submit' id='clXbackID10pr' value='<' onclick='clXback10pr()'> X (alt) <input class='handonbutton' type='submit' id='clXforwID10pr' value='>' onclick='clXforw10pr()'> <br> <input class='handonbutton' type='submit' id='clXspeedDownID10pr' value=' - ' onclick='clXspeedDown10pr()'> Speed: <span id="x-speed-label10pr">0.005</span> <input class='handonbutton' type='submit' id='clXspeedUpID10pr' value='+' onclick='clXspeedUp10pr()'> <br> <input type='range' step='0.001' id='x-speed-range10pr' name='x-speed-range10pr' value='0.005' min='0.001' max='0.2'/>
Здесь классы windowbox и handonbutton используются в css для задания стилей элементов.
Id будут использоваться для поиска конкретного элемента из программы на JS.
Функции, прописанные в onclick, будут использоваться для обработки событий в программе на JS. Теперь в файле controlas-button10pr.js создаем эти функции. Например, для элементов вращения относительно оси Х:
//--X choose direction--------------------------------------------------------------------------------
function clXforw10pr()
{
//Change direction.
if(ASDirectionX10pr == 1.0)
{
ASDirectionX10pr = -1.0;
document.getElementById( "clXbackID10pr" ).value = "<";
document.getElementById( "clXforwID10pr" ).value = " | | ";
}
//Start rotation.
else if(ASDirectionX10pr == 0.0)
{
ASDirectionX10pr = -1.0;
document.getElementById( "clXforwID10pr" ).value = " | | ";
}
//Stop rotation.
else if(ASDirectionX10pr == -1.0)
{
ASDirectionX10pr = 0.0;
document.getElementById( "clXforwID10pr" ).value = ">";
}
}
function clXback10pr()
{
…
}
//--X choose speed--------------------------------------------------------------------------------
$(document).ready(function()
{
$("#x-speed-range10pr").change(function()
{
var range = $(this);
var value = parseFloat(range.val());
ASspeedX10pr = value;
$( "#x-speed-label10pr" ).html( ASspeedX10pr.toFixed(3) );
});
});
function clXspeedDown10pr()
{
if(ASspeedX10pr.toFixed(3) > 0.001)
{
ASspeedX10pr /= 2;
$( "#x-speed-label10pr" ).html( ASspeedX10pr.toFixed(3) );
document.getElementById( "x-speed-range10pr" ).value = ASspeedX10pr.toFixed(3);
}
}
function clXspeedUp10pr()
{
if(ASspeedX10pr*2 <= MAX_SPEED)
{
ASspeedX10pr *= 2;
$( "#x-speed-label10pr" ).html( ASspeedX10pr.toFixed(3) );
document.getElementById( "x-speed-range10pr" ).value = ASspeedX10pr.toFixed(3);
}
}
Местами использован синтаксис библиотеки jQuery, подключенной ранее.
В коде мы, в зависимости от события, меняем для управления глобальные переменные. Зададим их в файле переменных:
//CONTROL. ---------------------------------------------------- //var angle10pr = 0; //Rotate. //START ANGLEs. var angleX10pr = 0.00; var angleY10pr = 0.00; var angleZ10pr = 0.00; //Speed by angle. var ASspeedX10pr = 0.005; var ASspeedY10pr = 0.005; var ASspeedZ10pr = 0.005; var MAX_SPEED = 0.2; //Direction. var ASDirectionX10pr = 0.0; var ASDirectionY10pr = 0.0; var ASDirectionZ10pr = 0.0; //View. var paused10pr = false; var useTexture10pr = true; var useLight10pr = true; //Zoom. var zoom10pr = 0.5; var MAX_ZOOM = 5.0;
И внесем обработку переменных в соответствующие места программы:
function setupWebGL10pr()
{
…
//Initial scene rotation.
//Set position of cam.
mat4.translate( mvMatrix10pr, [0.0, 0.0, -4.0]); //Zoom.
//Rotate view.
mat4.rotate( mvMatrix10pr, 0.50, [1.0, 0.0, 0.0]); //Around oX
mat4.rotate( mvMatrix10pr, -0.70, [0.0, 1.0, 0.0]); //Around oY
//Zoom view.
mat4.scale( mvMatrix10pr, [zoom10pr, zoom10pr, zoom10pr]);
//Plus scene rotation this frame.
mat4.rotate( mvMatrix10pr, angleX10pr, [1.0, 0.0, 0.0]);
mat4.rotate( mvMatrix10pr, angleY10pr, [0.0, 1.0, 0.0]);
mat4.rotate( mvMatrix10pr, angleZ10pr, [0.0, 0.0, 1.0]);
if( !paused10pr )
{
angleX10pr += ASDirectionX10pr * ASspeedX10pr;
angleY10pr += ASDirectionY10pr * ASspeedY10pr;
angleZ10pr += ASDirectionZ10pr * ASspeedZ10pr;
}
…
}
function getMatrixUniforms10pr()
{
…
//For control.
glProgram10pr.uDoTexturing10pr = gl10pr.getUniformLocation (glProgram10pr, "uDoTexturing10pr");
gl10pr.uniform1i (glProgram10pr.uDoTexturing10pr, 1);
glProgram10pr.uDoLighting10pr = gl10pr.getUniformLocation (glProgram10pr, "uDoLighting10pr");
gl10pr.uniform1i (glProgram10pr.uDoLighting10pr, 1);
}
Каждый шаг анимации мы поворачиваем сцену на углы angleX10pr, angleY10pr, angleZ10pr относительно начального положения сцены до старта вращений.
Отключение света сделаем по аналогии с отключением текстуры:
<script id="shader-fs-10pr" type="x-shader/x-fragment">
//Control.
uniform int uDoLighting10pr;
…
highp vec3 LightColor10pr;
…
void main(void)
{
if(uDoLighting10pr == 1)
LightColor10pr = vLight10pr;
else
LightColor10pr = vec3(0.8, 0.8, 0.8);
if(uDoTexturing10pr == 1)
{
highp vec4 tileColor = texture2D(uSampler10pr, vec2(vTextureCoord10pr.st));
highp vec4 logoColor = texture2D(uSampler210pr, vec2(vTextureCoord10pr.st));
highp vec4 textureColor = mix(tileColor, logoColor, logoColor.a);
gl_FragColor = vec4(textureColor.xyz * LightColor10pr, textureColor.a);
}
else
gl_FragColor = vec4(vColor10pr.xyz * LightColor10pr, vColor10pr.a);
}
</script>
Клавиатура
Для управления с клавиатуры дополним файл controlas-keyboard10pr.js обработкой кодов клавиш. Вызываются те же самые функции как и на инерфейсе выше:
document.addEventListener("keyup", myKeyupFunction10pr);
function myKeyupFunction10pr(evt)
{
//Keyboard keys ('T' = 't' = rus 'н' = rus 'Н').
switch(evt.keyCode)
{
//Direction.-----------------------------------------------
case 105: //'NumLock9 - Up'
clXforw10pr(); break;
case 97: //'NumLock1 - Down'
clXback10pr(); break;
case 102: //'NumLock6 - Right'
clYforw10pr(); break;
case 100: //'NumLock4 - Left'
clYback10pr(); break;
case 99: //'NumLock3 - Down'
clZforw10pr(); break;
case 103: //'NumLock7 - Up'
clZback10pr(); break;
//Speed.----------------------------------------------------
case 69: //'E'
clXspeedUp10pr(); break;
case 90: //'Z'
clXspeedDown10pr(); break;
case 68: //'D'
clYspeedUp10pr(); break;
case 65: //'A'
clYspeedDown10pr(); break;
case 81: //'Q'
clZspeedUp10pr(); break;
case 67: //'C'
clZspeedDown10pr(); break;
//Settings.------------------------------------------------------
case 80: //'P'
pause10pr(); break;
case 101: //'NumLock5'
StopRotation(); break;
case 96: //'NumLock0'
SetEqualSpeeds10pr(); break;
case 49: //'1'
SetInitialSettings10pr(); break;
//Zoom. ------------------------------------------------------
case 190: //'>'
document.getElementById ( "clZoomUpID10pr" ).click(); break;
case 188: //'<'
document.getElementById ( "clZoomDownID10pr" ).click(); break;
//Texture.------------------------------------------------------
case 84: //'T'
document.getElementById ( "clTextureBoxID10pr" ).click(); break;
//Light. ------------------------------------------------------
case 76: //'L'
document.getElementById ( "clLightingBoxID10pr" ).click(); break;
default:
break;
}
}
Мышь
На компьютере удобнее всего управлять объектом с помощью мыши. Необходимо вращать модель вокруг вертикальной оси при перетаскивании мышью влево-вправо. А также вращать вокруг оси, параллельной горизонту, при перетаскивании мышью вверх-вниз.
В файле controlas-mouse10pr.js добавляем реакции на нажатие, удержание, и отпускание любой кнопки мыши и прокручивание колеса:
$(document).ready(function()
{
$("#my-canvas10pr").on("mousedown", function (e)
{
capture10pr = true;
justStartCapture10pr = true;
start10pr = [e.pageX, e.pageY];
});
$("#my-canvas10pr").mousemove(function(e)
{
if(capture10pr)
{
//Delta of mouse move.
var dx = (e.pageX - start10pr[0]);
var dy = (e.pageY - start10pr[1]);
//Detect Left-Right or Up-Down mouse move.
if(justStartCapture10pr == true)
{
justStartCapture10pr = false;
if( Math.abs(dx) > Math.abs(dy) )
startedRotAroundY10pr = true;
else
{
if(
(Math.PI/4 < angleY10pr && angleY10pr <= 3*Math.PI/4) ||
(5*Math.PI/4 < angleY10pr && angleY10pr <= 7*Math.PI/4) ||
(-Math.PI/4 > angleY10pr && angleY10pr >= -3*Math.PI/4) ||
(-5*Math.PI/4 > angleY10pr && angleY10pr >= -7*Math.PI/4) )
startedRotAroundZ10pr = true;
else
startedRotAroundX10pr = true;
}
}
//Increase angles.
if(startedRotAroundY10pr)
{
dx *= 2*Math.PI/180.0; //Decrease delta for less mouse sensitivity.
angleY10pr += dx; //Increase an angle.
start10pr[0] = e.pageX; //Update start positions.
//For future next X or Z rotation count.
if(angleY10pr >= 2*Math.PI)
angleY10pr -= 2*Math.PI;
if(angleY10pr <= -2*Math.PI)
angleY10pr += 2*Math.PI;
}
else if(startedRotAroundX10pr == true)
{
//Move Up-Down instead of Down-Up.
if( (Math.PI/2 < angleY10pr && angleY10pr <= 3*Math.PI/2) ||
(-Math.PI/2 > angleY10pr && angleY10pr >= -3*Math.PI/2) )
dy *= -1;
dy *= 2*Math.PI/180.0;
angleX10pr += dy;
start10pr[1] = e.pageY;
}
else // startedRotAroundZ10pr == true
{
//Move Up-Down instead of Down-Up.
if( (Math.PI < angleY10pr && angleY10pr <= 2*Math.PI) ||
( 0 > angleY10pr && angleY10pr >= -Math.PI) )
dy *= -1;
dy *= 2*Math.PI/180.0;
angleZ10pr += dy;
start10pr[1] = e.pageY;
}
}
});
$("#my-canvas10pr").on("mouseup", function (e)
{
capture10pr = false;
justStartCapture10pr = false;
startedRotAroundY10pr = false;
startedRotAroundX10pr = false;
startedRotAroundZ10pr = false;
});
$("#my-canvas10pr").on( "mousewheel", function (e)
{
adjustZoom10pr( window.event.wheelDelta );
}).on( "DOMMouseScroll", function (e)
{ //For Firefox.
adjustZoom10pr( e.originalEvent.detail * -1.0);
});
});
//For mouse wheel.
function adjustZoom10pr(delta)
{
if(delta > 0)
zoom10pr += 0.1;
else
{
zoom10pr -= 0.1;
if(zoom10pr < 0.01)
zoom10pr = 0.1;
}
document.getElementById("zoom-range10pr").value = zoom10pr.toFixed(1);
$("#zoom-label10pr").html(zoom10pr.toFixed(1));
}
В коде выше приведено упрощённое вращение вокруг оси Y вместо вертикальной оси, а также упрощенное вращение вокруг X или Z – вместо горизонтальной.
Использованы глобальные переменные:
var capture10pr = false; var justStartCapture10pr = false; var startedRotAroundX10pr = false; var startedRotAroundY10pr = false; var startedRotAroundZ10pr = false; var start10pr = 0;
Каждый раз, при начале вращения, мы определяем, в какую сторону преимущественно двинули мышь. Если вправо-влево движение больше, то вращаем вокруг вертикальной оси:
if ( Math.abs (dx) > Math.abs (dy) )
Иначе вращаем вокруг одной из горизонтальных осей, направление которой из них меньше отличается от линии горизонта, согласно рисунку:

Желтый объект на рисунке уже будет поворачиваться вокруг оси Z.
Итак, за данный шаг мы добавили интерфейс для управления сценой с помощью кнопок, клавиатуры и мыши. Очевидно, сложность вызывает свободное вращение вокруг вертикальной оси и вокруг оси, параллельной горизонту. По окончанию вращения нам необходимо рассчитать новое положение этих осей. Скоро попытаемся понять, как-то это делается через кватернионы вращения.