Pull to refresh

Музейные приключения с телефоном

Reading time11 min
Views10K
Приветствую уважаемых читателей. Меня зовут Андрей, в последнее время я работаю в Центральном Музее Великой Отечественной войны. Помимо создания обычных музейных занимаюсь ещё и разработкой виртуальных выставок. Поэтому в своём обзоре Nokia Lumia 1520 я покажу, как можно использовать данный телефон для создания небольшой виртуальной экскурсии, используя 20-мегапиксельную камеру и JavaScript библиотеку Three.JS.

Но для начала давайте разберёмся, что попало ко мне в руки на несколько коротких дней тестирования. Вот как выглядит устройство из симпатичной синенькой коробочки.

image

Двумя основными признаками Lumia 1520 являются 20-мегапиксельная камера и операционная система Windows Phone 8. Кроме того, хотелось бы отметить аккумулятор, который мне удалось разрядить за несколько дней использования всего на 20-30%. Из странного – не получилось указать свою учётную запись Microsoft, даже не удалось создать новую, по каким-то невнятным причинам вроде отсутствия соединения или ошибок сервера. Без этого интерес к WP8 быстренько угас, потому что нельзя было установить ни одного приложения и проверить, чего стоит эта система. Впрочем, цель тестирования несколько иная – показать, как данным телефоном можно воспользоваться для создания виртуальной выставки в реальной, простите за каламбур, обстановке. А для того, чтобы было ещё интереснее, добавлю возможность использования шлема Oculus Rift.

Подходящей для сего действа мною была выбрана площадка боевой техники на прилегающей к музею территории:

image
Фотография снята на Nokia Lumia 1520 и слегка обрезана в GIMP`е

Итак, что же требуется сделать?

1. Снять комплект фотографий для каждой точки обзора.
2. Получить из каждого комплекта панорамную фотографию.
3. При помощи ThreeJS показать то, что у нас получилось на предыдущих этапах.

Полученный результат можно разместить на сайте, а если возникнет желание можно превратить в самостоятельную программу при помощи шаблона HTML5 Application из небезызвестного Qt5 или какого-нибудь Awesomium`а.

Прогулявшись площадке с техникой, я запечатлел интересные (с моей точки зрения) места на камеру Lumia 1520, которая называется PureView, и вполне соответствует своему названию.

Способ съёмки весьма прост – покрутившись на одном месте нужно снять серию фотографий с нахлёстом на предыдущую в 30-50%, для того чтобы получаемые панорамные фотографии каждой точки обзора склеивались правильно и без особых недостатков.

В процессе перехода от одной обзорной точки к другой удалось оценить, как телефон лежит в руке. Клавиши управления звуком, блокировкой телефона и камерой оказались прямо под пальцами левой руки:

image

Как же выглядят фотографии, которые нужно получить на первом этапе? Вот несколько примеров фотографий, сделанных камерой этого телефона. Обратите, пожалуйста, внимание на изменение угла вращения вокруг вертикальной оси:



Таким образом, были получены серии снимков 5 точек обзора площадки боевой техники, расположенной на Поклонной горе.

Самое время приступить обработке полученных фотографий, то есть перейти ко второму пункту моего небольшого плана. Для того, чтобы передать сохранённые фотографии из обширной памяти телефона на компьютер, я воспользовался USB кабелем из синенькой коробочки стандартного комплекта поставки. Пока передавались фотографии, я немного послушал музыку со своей карты памяти, которая поддерживается данным аппаратом. Кроме кабеля в комплекте имеются наушники с приятным на ощупь проводом и хорошим звуком:



Открыв папку с фотографиями я обнаружил необычный способ их сохранения. Для каждой фотографии имеется два графических файла, отличающихся разрешением: первый поменьше – 3072 на 1728, второй же – 5376 на 3024. Предлагаю использовать файлы с большим разрешением.

Далее требуется склеить серии фотографий в панорамный вид. Есть несколько программ, которые позволяют сделать это, среди них есть хорошие бесплатные. Можно отметить две бесплатные программы для подготовки панорамных фотографий: Hugin и Microsoft ICE.

Первая в освоении чуть сложнее, чем вторая, поэтому для быстрого получения результата воспользуемся Microsoft Image Composite Editor. Выделил фотографии с высоким разрешением, перетащил в рабочую область ICE, указал нужные настройки для получаемого файла – готово! В параметрах экспорта указываем ширину 4096, а нажав на кубик (обведён красным) – указываем, что нам требуется сферическая проекция, жмём кнопку Apply, а потом кнопку Export to disk…



Полученную панорамную фотографию уже почти можно использовать, нужно только привести её к размеру 4096x2048, что легко можно сделать в любом графическом редакторе типа GIMP или Paint.NET. Для этого просто создаём новый файл размером 4096x2048 с чёрным фоном и по центру накладываем склеенную панорамную фотографию точки обзора. Кстати, обратите внимание, если забыть, откуда началась съёмка и не вернуться в неё, то панорамная фотография в итоге не «замкнётся», как на скриншоте:



Далее с помощью Three.js сделанные панорамные фотографии используем как текстуру для сферы, внутри которой можно вращать камеру. Обратите внимание, так как я сделал только один ряд фотографий для каждой точки обзора, вращать камеру можно будет только вокруг вертикальной оси. Однако, при желании, изменив угол съёмки м сделать панорамные фотографии с большим углом обзора, то есть можно будет смотреть вверх и вниз. Впрочем, это вопрос не принципиального характера и кардинально процесс создания панорамы не изменится.

Взгляните на код файла index.html:

index.html
<!DOCTYPE html>
<html lang="en">
	<head>
		<title>Площадка боевой техники, Поклонная гора, сделано с помощью Nokia Lumia 1520</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<style>
			body {
				background-color: #000000;
				margin: 0px;
				overflow: hidden;
			}

			a {
				color: #ffffff;
			}
			
			#footer {
				position: absolute;
				bottom: 100px;
				left:100px;
			}
			
			#text {
				text-shadow: -10px 10px 0px #00e6e6,
                 -20px 20px 0px #01cccc,
                 -30px 30px 0px #00bdbd;
				 position: absolute;
				 top: 100px;
				 right:100px;
			}
			
			.button {
				-moz-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.15);
				-webkit-box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.15);
  				box-shadow: inset 0px 1px 0px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.15);
  				background-color: #EEE;
  				background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZiZmJmYiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2UxZTFlMSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
  				background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #fbfbfb), color-stop(100%, #e1e1e1));
  				background: -moz-linear-gradient(top, #fbfbfb, #e1e1e1);
  				background: -webkit-linear-gradient(top, #fbfbfb, #e1e1e1);
  				background: linear-gradient(to bottom, #fbfbfb, #e1e1e1);
  				display: inline-block;
  				vertical-align: middle;
  				*vertical-align: auto;
  				*zoom: 1;
  				*display: inline;
  				border: 1px solid #d4d4d4;
  				height: 32px;
  				line-height: 30px;
  				padding: 0px 25.6px;
  				font-weight: 300;
  				font-size: 14px;
  				font-family: "Helvetica Neue Light", "Helvetica Neue", "Helvetica", "Arial", "Lucida Grande", sans-serif;
  				color: #666;
  				text-shadow: 0 1px 1px white;
  				margin: 0;
  				text-decoration: none;
  				text-align: center; 
			}
  				/* line 44, ../scss/partials/_buttons.scss */
			.button:hover, .button:focus {
    			background-color: #EEE;
    			background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2RjZGNkYyIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
    			background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #ffffff), color-stop(100%, #dcdcdc));
    			background: -moz-linear-gradient(top, #ffffff, #dcdcdc);
    			background: -webkit-linear-gradient(top, #ffffff, #dcdcdc);
    			background: linear-gradient(to bottom, #ffffff, #dcdcdc); 
			}
			
  			/* line 48, ../scss/partials/_buttons.scss */
  			.button:active {
    			-moz-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3), 0px 1px 0px white;
    			-webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3), 0px 1px 0px white;
    			box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.3), 0px 1px 0px white;
    			text-shadow: 0px 1px 0px rgba(255, 255, 255, 0.4);
    			background: #eeeeee;
    			color: #bbbbbb; 
			}
			
  			/* line 54, ../scss/partials/_buttons.scss */
  			.button:focus {
    			outline: none; 
			}

			/* line 60, ../scss/partials/_buttons.scss */
			input.button, button.button {
  				height: 34px;
  				cursor: pointer;
  				-webkit-appearance: none; 
			}

			/* line 67, ../scss/partials/_buttons.scss */
			.button-block {
				display: block; 
			}

			/* line 72, ../scss/partials/_buttons.scss */
			.button.disabled,
			.button.disabled:hover,
			.button.disabled:focus,
			.button.disabled:active,
			input.button:disabled,
			button.button:disabled {
				-moz-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
  				-webkit-box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
  				box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.1);
  				filter: progid:DXImageTransform.Microsoft.Alpha(Opacity=80);
  				opacity: 0.8;
  				background: #EEE;
  				border: 1px solid #DDD;
  				text-shadow: 0 1px 1px white;
  				color: #CCC;
  				cursor: default;
  				-webkit-appearance: none; 
			}

			/* line 89, ../scss/partials/_buttons.scss */
			.button-wrap {
    			background: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4gPHN2ZyB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGRlZnM+PGxpbmVhckdyYWRpZW50IGlkPSJncmFkIiBncmFkaWVudFVuaXRzPSJvYmplY3RCb3VuZGluZ0JveCIgeDE9IjAuNSIgeTE9IjAuMCIgeDI9IjAuNSIgeTI9IjEuMCI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iI2UzZTNlMyIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2YyZjJmMiIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIGZpbGw9InVybCgjZ3JhZCkiIC8+PC9zdmc+IA==');
    			background: -webkit-gradient(linear, 50% 0%, 50% 100%, color-stop(0%, #e3e3e3), color-stop(100%, #f2f2f2));
    			background: -moz-linear-gradient(top, #e3e3e3, #f2f2f2);
    			background: -webkit-linear-gradient(top, #e3e3e3, #f2f2f2);
    			background: linear-gradient(to bottom, #e3e3e3, #f2f2f2);
    			-moz-border-radius: 200px;
    			-webkit-border-radius: 200px;
    			border-radius: 200px;
    			-moz-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.04);
    			-webkit-box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.04);
    			box-shadow: inset 0px 1px 3px rgba(0, 0, 0, 0.04);
    			padding: 10px;
    			display: inline-block; 
			}

			/* line 195, ../scss/partials/_buttons.scss */
			.button-circle {
      			-moz-border-radius: 240px;
        		-webkit-border-radius: 240px;
        		border-radius: 240px;
        		-moz-box-shadow: inset 0px 1px 1px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.2);
        		-webkit-box-shadow: inset 0px 1px 1px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.2);
        		box-shadow: inset 0px 1px 1px rgba(255, 255, 255, 0.5), 0px 1px 2px rgba(0, 0, 0, 0.2);
        		width: 50px;
				line-height: 50px;
				height: 50px;
				padding: 0px;
				border-width: 4px;
				font-size: 16px; 
			}
		</style>
	</head>
	<body>
		<script>
			function loadPoint(file){
				texture = THREE.ImageUtils.loadTexture( 'textures/' + file + '.jpg', {}, function() {
					mesh.material.map = texture;
				} );
			}
			
			
			//функция для переключения способа просмотра: через монитор или шлем виртуальной реальности.
			function switchView () {
				oculus_enabled =! oculus_enabled;
				
				renderer.setSize( window.innerWidth, window.innerHeight );
				effect.setSize( window.innerWidth, window.innerHeight );
			}
		</script>
		
		
		<div id="container">
			
			<div id="footer">
				<!--кнопки для показа точек обзора -->
				<a href="#" class="button button-circle" onclick="loadPoint(1)">1</a>
				<a href="#" class="button button-circle" onclick="loadPoint(2)">2</a>
				<a href="#" class="button button-circle" onclick="loadPoint(3)">3</a>
				<a href="#" class="button button-circle" onclick="loadPoint(4)">4</a>
				<a href="#" class="button button-circle" onclick="loadPoint(5)">5</a>
				<a href="#" class="button button-circle" onclick="switchView()">OR</a>
			</div>
		</div>

		<script src="js/three.min.js"></script>
		<script src="js/OculusRiftEffect.js"></script>

		<script>

			var camera, scene, renderer, mesh, oculus_enabled = false;

			var isUserInteracting = false,
			onMouseDownMouseX = 0, onMouseDownMouseY = 0,
			lon = 0, onMouseDownLon = 0,
			lat = 0, onMouseDownLat = 0,
			phi = 0, theta = 0;

			init();
			animate();

			function init() {

				var container;

				container = document.getElementById( 'container' );

				camera = new THREE.PerspectiveCamera( 40, window.innerWidth / window.innerHeight, 1, 1100 );
				camera.target = new THREE.Vector3( 0, 0, 0 );

				scene = new THREE.Scene();

				var geometry = new THREE.SphereGeometry( 500, 60, 40 );
				geometry.applyMatrix( new THREE.Matrix4().makeScale( -1, 1, 1 ) );

				var material = new THREE.MeshBasicMaterial( {
					map: THREE.ImageUtils.loadTexture( 'textures/1.jpg' ) //обратите внимание, здесь указывается первая точка обзора, то есть просмотр можно начать с любой, достаточно указать другой файл с панорамой.
				} );

				mesh = new THREE.Mesh( geometry, material );
				
				scene.add( mesh );

				renderer = new THREE.WebGLRenderer();
				renderer.setSize( window.innerWidth, window.innerHeight );
				container.appendChild( renderer.domElement );
				
				effect = new THREE.OculusRiftEffect( renderer, {worldScale: 100} );
				effect.setSize( window.innerWidth, window.innerHeight );

				document.addEventListener( 'mousedown', onDocumentMouseDown, false );
				document.addEventListener( 'mousemove', onDocumentMouseMove, false );
				document.addEventListener( 'mouseup', onDocumentMouseUp, false );
				
				window.addEventListener( 'resize', onWindowResize, false );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );
				effect.setSize( window.innerWidth, window.innerHeight );

			}

			function onDocumentMouseDown( event ) {

				event.preventDefault();

				isUserInteracting = true;

				onPointerDownPointerX = event.clientX;
				onPointerDownPointerY = event.clientY;

				onPointerDownLon = lon;
				onPointerDownLat = lat;

			}

			function onDocumentMouseMove( event ) {

				if ( isUserInteracting === true ) {

					lon = ( onPointerDownPointerX - event.clientX ) * 0.1 + onPointerDownLon;
					lat = ( event.clientY - onPointerDownPointerY ) * 0.1 + onPointerDownLat;

				}

			}

			function onDocumentMouseUp( event ) {

				isUserInteracting = false;

			}

			function animate() {

				requestAnimationFrame( animate );
				update();

			}

			function update() {

				if ( isUserInteracting === false ) {

					lon += 0.1;

				}

				lat = Math.max( - 85, Math.min( 85, lat ) );
				phi = THREE.Math.degToRad( 90 - lat );
				theta = THREE.Math.degToRad( lon );

				camera.target.x = 500 * Math.sin( phi ) * Math.cos( theta );
				//camera.target.y = 500 * Math.cos( phi );   //При подготовке панорамных фотографий с большим углом обзора нужно раскомментировать данную строчку, чтобы можно было "смотреть" вверх и вниз.
				camera.target.z = 500 * Math.sin( phi ) * Math.sin( theta );

				camera.lookAt( camera.target );
				
				if (oculus_enabled)
				{
					effect.render( scene, camera );
				} else
				{
					renderer.render( scene, camera );
				}
					
			}
		//присылайте, пожалуйста, ваши вопросы на электронную почту andreyazbarov@gmail.com
		</script>
	</body>
</html>




В секции head указаны стили кнопок, которые взяты у http://alexwolfe.github.io/Buttons/.

Вращение вокруг горизонтальной оси заблокировано, чтобы не показывать недостающие куски панорамы. Если подготовить панорамные фотографии с большим углом обзора по вертикали, то в функции update нужно раскомментировать одну строчку.

Изначально код взят из примера three.js – panorama demo и дополнен.

Для отображения виртуальной экскурсии через шлем Oculus Rift я воспользовался библиотекой от troffmo5 (http://github.com/troffmo5).

Скачать весь код можно с Google Drive по этой ссылке.

Просмотреть полученный результат можно по этой ссылке.



Кнопка с загадочными буквами OR позволяет включить отображение для шлема Oculus Rift, а кнопки с цифрами – переходить от одной точки обзора к другой.

В дальнейшем можно улучшить подобную мини-экскурсию, добавив описание к каждой точке обзора и к каждому представленному экспонату, или, например, добавить какой-нибудь удобный способ управления просмотром.

Надеюсь, данный пример сможет помочь кому-либо в повседневной деятельности.
Tags:
Hubs:
+11
Comments11

Articles