Строим 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.
Итак, за данный шаг мы добавили интерфейс для управления сценой с помощью кнопок, клавиатуры и мыши. Очевидно, сложность вызывает свободное вращение вокруг вертикальной оси и вокруг оси, параллельной горизонту. По окончанию вращения нам необходимо рассчитать новое положение этих осей. Скоро попытаемся понять, как-то это делается через кватернионы вращения.