Last active
March 6, 2023 15:31
-
-
Save septagon/e7e19b85660f2915b0ce018b049d8017 to your computer and use it in GitHub Desktop.
One-file Babylon.js 360 photo viewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<!-- Assembled from assorted Babylon.js samples --> | |
<!-- To run from a local folder, do something like the following. --> | |
<!-- npm install http-server --> | |
<!-- .\node_modules\.bin\http-server -a localhost -p 8000 -c-1 --> | |
<!-- then open localhost:8000/index.html (if that's what you named --> | |
<!-- it) in your browser, with your equirectangular 360 photo --> | |
<!-- named "image.jpg" in the same directory. --> | |
<html> | |
<head> | |
<meta http-equiv="Content-Type" content="text/html" charset="utf-8"/> | |
<title>Basic 360 Photo Viewer</title> | |
<!--- Link to the last version of BabylonJS ---> | |
<script src="https://cdn.babylonjs.com/babylon.js"></script> | |
<style> | |
html, body { | |
overflow: hidden; | |
width : 100%; | |
height : 100%; | |
margin : 0; | |
padding : 0; | |
} | |
#renderCanvas { | |
width : 100%; | |
height : 100%; | |
touch-action: none; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas id="renderCanvas"></canvas> | |
<script> | |
var addSphericalPanningCameraToScene = function (scene, canvas) { | |
// Set cursor to grab. | |
scene.defaultCursor = "grab"; | |
// Add the actual camera to the scene. Since we are going to be controlling it manually, | |
// we don't attach any inputs directly to it. | |
// NOTE: We position the camera at origin in this case, but it doesn't have to be there. | |
// Spherical panning should work just fine regardless of the camera's position. | |
var camera = new BABYLON.FreeCamera("camera", BABYLON.Vector3.Zero(), scene); | |
// Ensure the camera's rotation quaternion is initialized correctly. | |
camera.rotationQuaternion = BABYLON.Quaternion.Identity(); | |
// The spherical panning math has singularities at the poles (up and down) that cause | |
// the orientation to seem to "flip." This is undesirable, so this method helps reject | |
// inputs that would cause this behavior. | |
var isNewForwardVectorTooCloseToSingularity = v => { | |
const TOO_CLOSE_TO_UP_THRESHOLD = 0.99; | |
return Math.abs(BABYLON.Vector3.Dot(v, BABYLON.Vector3.Up())) > TOO_CLOSE_TO_UP_THRESHOLD; | |
} | |
// Local state variables which will be used in the spherical pan method; declared outside | |
// because they must persist from frame to frame. | |
var ptrX = 0; | |
var ptrY = 0; | |
var inertiaX = 0; | |
var inertiaY = 0; | |
// Variables internal to spherical pan, declared here just to avoid reallocating them when | |
// running. | |
var priorDir = new BABYLON.Vector3(); | |
var currentDir = new BABYLON.Vector3(); | |
var rotationAxis = new BABYLON.Vector3(); | |
var rotationAngle = 0; | |
var rotation = new BABYLON.Quaternion(); | |
var newForward = new BABYLON.Vector3(); | |
var newRight = new BABYLON.Vector3(); | |
var newUp = new BABYLON.Vector3(); | |
var matrix = new BABYLON.Matrix.Identity(); | |
// The core pan method. | |
// Intuition: there exists a rotation of the camera that brings priorDir to currentDir. | |
// By concatenating this rotation with the existing rotation of the camera, we can move | |
// the camera so that the cursor appears to remain over the same point in the scene, | |
// creating the feeling of smooth and responsive 1-to-1 motion. | |
var pan = (currX, currY) => { | |
// Helper method to convert a screen point (in pixels) to a direction in view space. | |
var getPointerViewSpaceDirectionToRef = (x, y, ref) => { | |
BABYLON.Vector3.UnprojectToRef( | |
new BABYLON.Vector3(x, y, 0), | |
canvas.width, | |
canvas.height, | |
BABYLON.Matrix.Identity(), | |
BABYLON.Matrix.Identity(), | |
camera.getProjectionMatrix(), | |
ref); | |
ref.normalize(); | |
} | |
// Helper method that computes the new forward direction. This was split into its own | |
// function because, near the singularity, we may to do this twice in a single frame | |
// in order to reject inputs that would bring the forward vector too close to vertical. | |
var computeNewForward = (x, y) => { | |
getPointerViewSpaceDirectionToRef(ptrX, ptrY, priorDir); | |
getPointerViewSpaceDirectionToRef(x, y, currentDir); | |
BABYLON.Vector3.CrossToRef(priorDir, currentDir, rotationAxis); | |
// If the magnitude of the cross-product is zero, then the cursor has not moved | |
// since the prior frame and there is no need to do anything. | |
if (rotationAxis.lengthSquared() > 0) { | |
rotationAngle = BABYLON.Vector3.GetAngleBetweenVectors(priorDir, currentDir, rotationAxis); | |
BABYLON.Quaternion.RotationAxisToRef(rotationAxis, -rotationAngle, rotation); | |
// Order matters here. We create the new forward vector by applying the new rotation | |
// first, then apply the camera's existing rotation. This is because, since the new | |
// rotation is computed in view space, it only makes sense for a camera that is | |
// facing forward. | |
newForward.set(0, 0, 1); | |
newForward.rotateByQuaternionToRef(rotation, newForward); | |
newForward.rotateByQuaternionToRef(camera.rotationQuaternion, newForward); | |
return !isNewForwardVectorTooCloseToSingularity(newForward); | |
} | |
return false; | |
} | |
// Compute the new forward vector first using the actual input, both X and Y. If this results | |
// in a forward vector that would be too close to the singularity, recompute using only the | |
// new X input, repeating the Y input from the prior frame. If either of these computations | |
// succeeds, construct the new rotation matrix using the result. | |
if (computeNewForward(currX, currY) || computeNewForward(currX, ptrY)) { | |
// We manually compute the new right and up vectors to ensure that the camera | |
// only has pitch and yaw, never roll. This dependency on the world-space | |
// vertical axis is what causes the singularity described above. | |
BABYLON.Vector3.CrossToRef(BABYLON.Vector3.Up(), newForward, newRight); | |
BABYLON.Vector3.CrossToRef(newForward, newRight, newUp); | |
// Create the new world-space rotation matrix from the computed forward, right, | |
// and up vectors. | |
matrix.setRowFromFloats(0, newRight.x, newRight.y, newRight.z, 0); | |
matrix.setRowFromFloats(1, newUp.x, newUp.y, newUp.z, 0); | |
matrix.setRowFromFloats(2, newForward.x, newForward.y, newForward.z, 0); | |
BABYLON.Quaternion.FromRotationMatrixToRef(matrix.getRotationMatrix(), camera.rotationQuaternion); | |
} | |
}; | |
// The main panning loop, to be run while the pointer is down. | |
var sphericalPan = () => { | |
pan(scene.pointerX, scene.pointerY); | |
// Store the state variables for use in the next frame. | |
inertiaX = scene.pointerX - ptrX; | |
inertiaY = scene.pointerY - ptrY; | |
ptrX = scene.pointerX; | |
ptrY = scene.pointerY; | |
} | |
// The inertial panning loop, to be run after the pointer is released until inertia | |
// runs out, or until the pointer goes down again, whichever happens first. Essentially | |
// just pretends to provide a decreasing amount of input based on the last observed input, | |
// removing itself once the input becomes negligible. | |
const INERTIA_DECAY_FACTOR = 0.9; | |
const INERTIA_NEGLIGIBLE_THRESHOLD = 0.5; | |
var inertialPanObserver; | |
var inertialPan = () => { | |
if (Math.abs(inertiaX) > INERTIA_NEGLIGIBLE_THRESHOLD || Math.abs(inertiaY) > INERTIA_NEGLIGIBLE_THRESHOLD) { | |
pan(ptrX + inertiaX, ptrY + inertiaY); | |
inertiaX *= INERTIA_DECAY_FACTOR; | |
inertiaY *= INERTIA_DECAY_FACTOR; | |
} | |
else { | |
scene.onBeforeRenderObservable.remove(inertialPanObserver); | |
} | |
}; | |
// Enable/disable spherical panning depending on click state. Note that this is an | |
// extremely simplistic way to do this, so it gets a little janky on multi-touch. | |
var sphericalPanObserver; | |
var pointersDown = 0; | |
scene.onPointerDown = () => { | |
pointersDown += 1; | |
if (pointersDown !== 1) { | |
return; | |
} | |
// Disable inertial panning. | |
scene.onBeforeRenderObservable.remove(inertialPanObserver); | |
// Switch cursor to grabbing. | |
scene.defaultCursor = "grabbing"; | |
// Store the current pointer position to clean out whatever values were left in | |
// there from prior iterations. | |
ptrX = scene.pointerX; | |
ptrY = scene.pointerY; | |
// Enable spherical panning. | |
sphericalPanObserver = scene.onBeforeRenderObservable.add(sphericalPan); | |
} | |
scene.onPointerUp = () => { | |
pointersDown -= 1; | |
if (pointersDown !== 0) { | |
return; | |
} | |
// Switch cursor to grab. | |
scene.defaultCursor = "grab"; | |
// Disable spherical panning. | |
scene.onBeforeRenderObservable.remove(sphericalPanObserver); | |
// Enable inertial panning. | |
inertialPanObserver = scene.onBeforeRenderObservable.add(inertialPan); | |
} | |
}; | |
window.addEventListener('DOMContentLoaded', function(){ | |
// get the canvas DOM element | |
var canvas = document.getElementById('renderCanvas'); | |
// load the 3D engine | |
var engine = new BABYLON.Engine(canvas, true); | |
var createScene = function () { | |
var scene = new BABYLON.Scene(engine); | |
var dome = new BABYLON.PhotoDome( | |
"testdome", | |
"./image.jpg", | |
{ | |
resolution: 32, | |
size: 1000 | |
}, | |
scene | |
); | |
addSphericalPanningCameraToScene(scene, canvas); | |
// The following block of code is a curious little trick to get the spherical panning | |
// function to play nice with Babylon's built-in default VR experience. All it does | |
// is effectively override the default VR experience's non-VR camera with the | |
// spherical panning one, setting that as the active camera to start with and | |
// setting it again whenever VR mode exits. Note that this is not actually a part | |
// of spherical panning; it's just a way to get it to not interfere with/be overridden | |
// by the VR experience. | |
var sphericalPanningCamera = scene.activeCamera; | |
var vrExperience = scene.createDefaultVRExperience(); | |
if (!vrExperience.isInVRMode) { | |
scene.activeCamera = sphericalPanningCamera; | |
} | |
vrExperience.onExitingVRObservable.add(() => { | |
setTimeout(() => { | |
scene.activeCamera = sphericalPanningCamera; | |
}, 0); | |
}); | |
return scene; | |
}; | |
// call the createScene function | |
var scene = createScene(); | |
// run the render loop | |
engine.runRenderLoop(function(){ | |
scene.render(); | |
}); | |
// the canvas/window resize event handler | |
window.addEventListener('resize', function(){ | |
engine.resize(); | |
}); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment