Подумать только

logoas CogITas

Dubito - Ergo - Sum

× Error from canvas.getContext(): Maybe your browser or hardware (GPU) does not appear to support WebGL. The Canvas for WebGL below will be empty.
Mouse и Num Lock

Каждой программе по интерфейсу!

Строим 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>

   Из шаблона видно:

  • для экстра маленьких экранов (xs) каждая колонка будет на всю ширину экрана (занимать все 12 колонок);
  • для маленьких экранов (sm) третья колонка будет на весь экран, а первые две в пропорциях 8-4 для Viewer и Controller (в сумме 12);
  • для средних (md) и больших(lg) экранов пропорции 5-3-4 (в сумме 12).

   Три колонки Help, Viewer, Controller в какой-то мере олицетворяют разделение как в шаблоне MVC.

Model–View–Controller («Модель–Вид–Контроллер») — схема разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента. Модификация каждого компонента может осуществляться независимо.
  • Модель предоставляет данные и реагирует на команды контроллера, изменяя своё состояние;
  • Видотвечает за отображение данных модели пользователю, реагируя на изменения модели;
  • Контроллеринтерпретирует действия пользователя, оповещая модель о необходимости изменений.

   Напишем в 3-ем столбце интерфейса информацию о модели реагирования на кнопки в виде помощи Help:

Колонка подсказок Help

Колонка подсказок Help

 

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

Колонка управления Controller

Колонка управления Controller

 

   Например, для создания элементов вращения относительно оси Х, используем следующий код:

<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='&nbsp;-&nbsp;' 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;
	}
}
   На момент написания программы в браузере Chrome иногда переставали обрабатываться события keyup при нажитии буквенных клавиш. Лечилось это перезапуском браузера. Но можно когда-нибудь, для надежности, доработать программу для альтернативного восприятия событий keypress, как в программе здесь.

 

Мышь

   На компьютере удобнее всего управлять объектом с помощью мыши. Необходимо вращать модель вокруг вертикальной оси при перетаскивании мышью влево-вправо. А также вращать вокруг оси, параллельной горизонту, при перетаскивании мышью вверх-вниз.

   В файле 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) )

   Иначе вращаем вокруг одной из горизонтальных осей, направление которой из них меньше отличается от линии горизонта, согласно рисунку:

Выбор вращения по осям X или Z

Выбор вращения по осям X или Z

 

   Желтый объект на рисунке уже будет поворачиваться вокруг оси Z.

 

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

 

Интерфейс

<< Предыдущий шаг

Следующий шаг >>

Все статьи


Оставить комментарий

Ваш email не будет отображаться. Обязательные поля помечены *

(Чтобы установить аватар, необходимо зарегистрировать свой e-mail на gravatar.com. Как это сделать, написано в статье http://cogitas.ru/robots-avatar-icon-wordpress.html)