Хорошее дополнение к вашему следующему веб-приложению
Идея сделать такую штуку пришла мне в голову, когда я увидел похожие демо на официальном сайте Threejs.
Так что я хотел просто скопировать и вставить код и посмотреть, что получилось, но столкнулся с проблемой, что при переносе чужого кода в мой редактор приложение не работало должным образом, и в этих демонстрациях использовалась не стандартная математика. Как-то странно подумал я. В конце концов я решил написать весь код с нуля, а именно получить чужой код и настроить его с помощью надлежащей стандартной математики.
Что такое скайбокс
Эта техника, когда вы находитесь внутри виртуального 3D-мира, состоящего из простых изображений, называется скайбоксом.
Концепция скайбокса очень проста. Скайбокс - это просто ящик или сфера с небом (любым изображением) нарисованным на нем. Мы помещаем камеру внутрь коробки, и кажется, что на заднем плане небо.
В этом уроке мы используем сферу, потому что нам нужно только одно изображение вместо шести. Мы будем использовать панорамные изображения.
Настраивать
Единственный обязательный элемент HTML — это элемент ‹div› с атрибутом id.
<div id="app"></div>
Единственный требуемый CSS — сделать элемент canvas черным фоном. Также избавьтесь от полей тела и установите переполнение: скрыто, чтобы настроить границы тела и холста на экране.
body { margin: 0; overflow: hidden; } canvas{ width: 100%; height: 100% }
Теперь приступим к самой интересной части JavaScript.
Мы инициализируем общие константы, которые нам скоро понадобятся.
const app = document.getElementById('app'); let scene; let renderer; let camera; let windowWidth = window.innerWidth; let windowHeight = window.innerHeight; let fov = 75; let lon = 0; let lat = 0; let phi = 0; let theta = 0; const nearPlane = 1; const farPlane = 1000; let aspect = windowWidth / windowHeight; const dpr = window.devicePixelRatio; let sphereMaterial; let isManual = false; let lastMouseX = 0; let lastMouseY = 0; let lastLon = 0; let lastLat = 0;
Далее мы видим базовую настройку для three.js, как вы, вероятно, знаете из других руководств. Здесь мы собираемся создать сцену, камеру, рендерер и функции инициализации, которые мы увидим ниже. Мы используем текущую область просмотра в качестве размера средства визуализации и добавляем ее на страницу в качестве холста.
function init() { renderer = new THREE.WebGLRenderer(); renderer.setSize(windowWidth, windowHeight); renderer.setPixelRatio(dpr); app.appendChild(renderer.domElement); scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera(75, aspect, nearPlane, farPlane); camera.target = new THREE.Vector3(0, 0, 0); createSphere(); initEventListeners(); animate(); } window.onload = init;
Прежде чем мы продолжим, я просто хочу отметить, что все функциональные возможности этого приложения находятся в пределах функций, которые достигают модульности.
Поскольку мы собираемся использовать сферу, мы напишем еще одну служебную функцию. Код прост.
function createSphere() { const sphere = new THREE.SphereGeometry(100, 100, 40); // or we can use just sphere.scale(-1, 1, 1); sphere.applyMatrix(new THREE.Matrix4().makeScale(-1, 1, 1)); sphereMaterial = new THREE.MeshBasicMaterial(); sphereMaterial.map = new THREE.TextureLoader().load(panoramasArray[panoramaNumber]) const sphereMesh = new THREE.Mesh(sphere, sphereMaterial); scene.add(sphereMesh); }
Как и в каждом уроке, нам нужно добавить функцию изменения размера холста при изменении размера окна.
function resizeWindow() { const width = window.innerWidth; const height = window.innerHeight; camera.aspect = width / height; camera.updateProjectionMatrix(); const canvasPixelWidth = canvas.width / dpr; const canvasPixelHeight = canvas.height / dpr; if (canvasPixelWidth !== width || canvasPixelHeight !== height) { renderer.setSize(width, height, false); } }
Теперь мы добавляем события для взаимодействия с нашей сценой, такие как движение мыши и добавление еще одного панорамного изображения на наш холст.
function initEventListeners() { document.addEventListener('mousedown', handleDocumentMouseDown, false); document.addEventListener('mousemove', handleDocumentMouseMove, false); document.addEventListener('mouseup', handleDocumentMouseUp, false); document.addEventListener('touchstart', handleDocumentMouseDown, false); document.addEventListener('touchmove', handleDocumentMouseMove, false); document.addEventListener('touchend', handleDocumentMouseUp, false); // when a user clicks on space we change panorama document.addEventListener('keyup', handleKeyUp); // Drag n Drop to load new panorama document.addEventListener('dragover', handleDragOver, false); document.addEventListener('dragenter', handleDragEnter, false); document.addEventListener('dragleave', handleDragLeave, false); document.addEventListener('drop', handleDrop, false); window.addEventListener('resize', resizeWindow); }
Ключевой частью является наша функция анимации. Здесь мы проверяем, активно ли событие mousemouse, если нет, то суммируем значение lon, преобразуем из сферических в декартовы координаты иобновляем цель камеры.
Мы сделать это из-за использования сферы, и нам нужно это преобразование, потому что наша камера ждет декартовых координат, чтобы наконец отобразить 2D-изображение. В каждом кадре мы добавляем значение широты, чтобы заставить камеру двигаться.
function animate() { requestAnimationFrame(animate); if(!isManual){ lat += 0.1; } phi = (90 - lat) * Math.PI / 180; theta = lon * Math.PI / 180; // Transform from Spherical to Cartesian coordinates camera.target.x = 500 * Math.sin(phi) * Math.cos(theta); camera.target.y = 500 * Math.sin(phi) * Math.sin(theta); camera.target.z = 500 * Math.cos(phi); camera.lookAt(camera.target); render(); } function render() { renderer.render(scene, camera); }
Далее я пишу обработчики событий. Это просто: обновите наши общие константы, используйте preventDefault для перетаскивания, как это показано в MDN, измените непрозрачность холста, когда мы перетаскиваем новое изображение, и сбрасывайте значения мыши и latLng, когда событие выполнено. Мы также определяем коэффициент 0,1 (эмпирически), чтобы замедлить изменение цели камеры, потому что цель камеры onmousemove меняется очень быстро.
function handleKeyUp(event) { if (event.code === 'Space') { panoramaNumber = (panoramaNumber + 1) % panoramasArray.length; sphereMaterial.map = new THREE.TextureLoader().load(panoramasArray[panoramaNumber]); } } // when the mouse is pressed, we switch to manual control and save current coordinates function handleDocumentMouseDown(event){ event.preventDefault(); isManual = true; const clientX = event.clientX || event.touches[ 0 ].clientX; const clientY = event.clientY || event.touches[ 0 ].clientY; lastMouseX = clientX; lastMouseY = clientY; lastLon = lon; lastLat = lat; } // when the mouse moves, if in manual control we adjust coordinates function handleDocumentMouseMove(event) { if (isManual) { const clientX = event.clientX || event.touches[0].clientX; const clientY = event.clientY || event.touches[0].clientY; lat = (clientX - lastMouseX) * 0.1 + lastLat; lon = (clientY - lastMouseY) * 0.1 + lastLon; } } // when the mouse is released, we turn manual control off function handleDocumentMouseUp(){ isManual = false; lastMouseX = 0; lastMouseY = 0; lastLon = 0; lastLat = 0; } function handleDragOver(event) { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'copy'; } function handleDragEnter(event) { event.preventDefault(); event.stopPropagation(); app.style.opacity = 0.5; } function handleDragLeave(event) { event.preventDefault(); event.stopPropagation(); app.style.opacity = 1; } function handleDrop(event) { event.preventDefault(); event.stopPropagation(); const reader = new FileReader(); reader.addEventListener('load', function(event) { sphereMaterial.map.image.src = event.target.result; sphereMaterial.map.needsUpdate = true; }, false ); reader.readAsDataURL(event.dataTransfer.files[0]); app.style.opacity = 1; }
Наконец, мы сбрасываем обработчики событий, это обычная практика.
function cleanup() { document.removeEventListener('mousedown', handleDocumentMouseDown, false); document.removeEventListener('mousemove', handleDocumentMouseMove, false); document.removeEventListener('mouseup', handleDocumentMouseUp, false); document.removeEventListener('touchstart', handleDocumentMouseDown, false); document.removeEventListener('touchmove', handleDocumentMouseMove, false); document.removeEventListener('touchend', handleDocumentMouseUp, false); document.removeEventListener('keyup', handleKeyUp); document.removeEventListener('dragover', handleDragOver, false); document.removeEventListener('dragenter', handleDragEnter, false); document.removeEventListener('dragleave', handleDragLeave, false); document.removeEventListener('drop', handleDrop, false); window.removeEventListener('resize', resizeWindow); } window.onunload = cleanup;
Конец
Чтобы посмотреть живую демонстрацию, пожалуйста, нажмите здесь.