-
-
Save Emanuele-Spatola/1e7b442cc1c4f08a0e0ccc716432cd7d to your computer and use it in GitHub Desktop.
Simple WebGL/GLSL video panorama 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> | |
<html> | |
<head> | |
<meta charset="utf-8"> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | |
<title>Simple WebGL/GLSL video panorama viewer</title> | |
<style> | |
body { | |
background-color: #000000; | |
margin: 0px; | |
overflow: hidden; | |
} | |
video { | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<canvas></canvas> | |
<video></video> | |
<script id="vs" type="x-shader/vertex"> | |
attribute vec3 position; | |
void main() { | |
gl_Position = vec4( position, 1.0 ); | |
} | |
</script> | |
<script id="fs" type="x-shader/fragment"> | |
uniform vec2 resolution; | |
uniform sampler2D texture; | |
uniform float fov; | |
uniform float yaw; | |
uniform float pitch; | |
const float M_PI = 3.141592653589793238462643; | |
const float M_TWOPI = 6.283185307179586476925286; | |
mat3 rotationMatrix(vec2 euler) | |
{ | |
vec2 se = sin(euler); | |
vec2 ce = cos(euler); | |
return mat3(ce.x, 0, -se.x, 0, 1, 0, se.x, 0, ce.x) * | |
mat3(1, 0, 0, 0, ce.y, -se.y, 0, se.y, ce.y); | |
} | |
vec3 toCartesian( vec2 st ) | |
{ | |
return normalize(vec3(st.x, st.y, 0.5 / tan(0.5 * radians(fov)))); | |
} | |
vec2 toSpherical(vec3 cartesianCoord) | |
{ | |
vec2 st = vec2( | |
atan(cartesianCoord.x, cartesianCoord.z), | |
acos(cartesianCoord.y) | |
); | |
if(st.x < 0.0) | |
st.x += M_TWOPI; | |
return st; | |
} | |
void main(void) | |
{ | |
vec2 sphericalCoord = gl_FragCoord.xy / resolution - vec2(0.5); | |
sphericalCoord.y *= -resolution.y / resolution.x; | |
vec3 cartesianCoord = rotationMatrix(radians(vec2(yaw + 180., -pitch))) * toCartesian(sphericalCoord); | |
gl_FragColor = texture2D(texture, toSpherical( cartesianCoord )/vec2(M_TWOPI, M_PI)); | |
} | |
</script> | |
<script> | |
/** | |
* Provides requestAnimationFrame in a cross browser way. | |
* paulirish.com/2011/requestanimationframe-for-smart-animating/ | |
*/ | |
window.requestAnimationFrame = window.requestAnimationFrame || ( function() { | |
return window.webkitRequestAnimationFrame || | |
window.mozRequestAnimationFrame || | |
window.oRequestAnimationFrame || | |
window.msRequestAnimationFrame || | |
function( callback, element ) { | |
window.setTimeout( callback, 1000 / 60 ); | |
}; | |
})(); | |
var canvas, | |
gl, | |
video, video_texture, video_started = false, | |
buffer, | |
vertex_shader, fragment_shader, currentProgram, | |
vertex_position, | |
parameters = { start_time : new Date().getTime(), | |
time : 0, | |
screenWidth : 0, | |
screenHeight: 0 }, | |
currentlyDragging = false, | |
currentDragX = 0, currentDragY = 0, | |
startDragX = 0, startDragY = 0, | |
xSpeed = 0, ySpeed = 0, | |
yaw = 0, pitch = 0, fov = 80; | |
init("video.ogv", "preview.jpg"); | |
animate(); | |
function init( video_url, preview_url ) { | |
video = document.querySelector( 'video' ); | |
video.addEventListener("canplaythrough", startVideo, true); | |
video.preload = "auto"; | |
video.loop = true; | |
video.src = video_url; | |
canvas = document.querySelector( 'canvas' ); | |
vertex_shader = document.getElementById('vs').textContent; | |
fragment_shader = document.getElementById('fs').textContent; | |
// Initialise WebGL | |
try { | |
gl = canvas.getContext( 'experimental-webgl' ); | |
} catch( error ) { } | |
if ( !gl ) { | |
throw "cannot create webgl context"; | |
} | |
// Create Vertex buffer (2 triangles) | |
buffer = gl.createBuffer(); | |
gl.bindBuffer( gl.ARRAY_BUFFER, buffer ); | |
gl.bufferData( gl.ARRAY_BUFFER, new Float32Array( [ - 1.0, - 1.0, 1.0, - 1.0, - 1.0, 1.0, 1.0, - 1.0, 1.0, 1.0, - 1.0, 1.0 ] ), gl.STATIC_DRAW ); | |
// Create texture for video | |
createTexture( preview_url ); | |
// Create Program | |
currentProgram = createProgram( vertex_shader, fragment_shader ); | |
handleResize(); | |
window.addEventListener( 'resize', handleResize, false ); | |
// Initialise interaction | |
canvas.onmousedown = handleMouseDown; | |
document.onmouseup = handleMouseUp; | |
canvas.onmousemove = handleMouseMove; | |
canvas.ondblclick = handleDoubleClick; | |
} | |
function animate() { | |
handleMouse(); | |
render(); | |
requestAnimationFrame( animate ); | |
} | |
function render() { | |
if ( !currentProgram ) return; | |
if( video_started ) | |
updateTexture(); | |
parameters.time = new Date().getTime() - parameters.start_time; | |
gl.clear( gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT ); | |
// Load program into GPU | |
gl.useProgram( currentProgram ); | |
gl.bindTexture(gl.TEXTURE_2D, video_texture); | |
// Set values to program variables | |
gl.uniform2f( gl.getUniformLocation( currentProgram, 'resolution' ), parameters.screenWidth, parameters.screenHeight ); | |
gl.uniform1i( gl.getUniformLocation( currentProgram, 'video' ), 0 ); | |
gl.uniform1f( gl.getUniformLocation( currentProgram, 'fov' ), 80 ); | |
gl.uniform1f( gl.getUniformLocation( currentProgram, 'yaw' ), yaw ); | |
gl.uniform1f( gl.getUniformLocation( currentProgram, 'pitch' ), -pitch ); | |
// Render geometry | |
gl.bindBuffer( gl.ARRAY_BUFFER, buffer ); | |
gl.vertexAttribPointer( vertex_position, 2, gl.FLOAT, false, 0, 0 ); | |
gl.enableVertexAttribArray( vertex_position ); | |
gl.drawArrays( gl.TRIANGLES, 0, 6 ); | |
gl.disableVertexAttribArray( vertex_position ); | |
} | |
function createShader( src, type ) { | |
var shader = gl.createShader( type ); | |
gl.shaderSource( shader, src ); | |
gl.compileShader( shader ); | |
if ( !gl.getShaderParameter( shader, gl.COMPILE_STATUS ) ) { | |
alert( ( type == gl.VERTEX_SHADER ? "VERTEX" : "FRAGMENT" ) + " SHADER:\n" + gl.getShaderInfoLog( shader ) ); | |
return null; | |
} | |
return shader; | |
} | |
function createProgram( vertex, fragment ) { | |
var program = gl.createProgram(); | |
var vs = createShader( vertex, gl.VERTEX_SHADER ); | |
var fs = createShader( '#ifdef GL_ES\nprecision highp float;\n#endif\n\n' + fragment, gl.FRAGMENT_SHADER ); | |
if ( vs == null || fs == null ) return null; | |
gl.attachShader( program, vs ); | |
gl.attachShader( program, fs ); | |
gl.deleteShader( vs ); | |
gl.deleteShader( fs ); | |
gl.linkProgram( program ); | |
if ( !gl.getProgramParameter( program, gl.LINK_STATUS ) ) { | |
alert( "ERROR:\n" + | |
"VALIDATE_STATUS: " + gl.getProgramParameter( program, gl.VALIDATE_STATUS ) + "\n" + | |
"ERROR: " + gl.getError() + "\n\n" + | |
"- Vertex Shader -\n" + vertex + "\n\n" + | |
"- Fragment Shader -\n" + fragment ); | |
return null; | |
} | |
return program; | |
} | |
function createTexture( image_src ) { | |
video_texture = gl.createTexture(); | |
video_texture.image = new Image(); | |
video_texture.image.onload = function() { | |
gl.bindTexture(gl.TEXTURE_2D, video_texture); | |
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video_texture.image); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
gl.bindTexture(gl.TEXTURE_2D, null); | |
} | |
video_texture.image.src = image_src; | |
} | |
function updateTexture() { | |
gl.bindTexture(gl.TEXTURE_2D, video_texture); | |
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, video); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
gl.bindTexture(gl.TEXTURE_2D, null); | |
} | |
function startVideo() { | |
video.play(); | |
video_started = true; | |
} | |
function handleResize( event ) { | |
canvas.width = window.innerWidth; | |
canvas.height = window.innerHeight; | |
parameters.screenWidth = canvas.width; | |
parameters.screenHeight = canvas.height; | |
gl.viewport( 0, 0, canvas.width, canvas.height ); | |
} | |
function handleDoubleClick(event) { | |
var isInFullScreen = (document.fullScreen || document.mozFullScreen || document.webkitIsFullScreen); | |
if(!isInFullScreen) { | |
if (canvas.requestFullscreen) { | |
canvas.requestFullscreen(); | |
} else if (canvas.mozRequestFullScreen) { | |
canvas.mozRequestFullScreen(); | |
} else if (canvas.webkitRequestFullscreen) { | |
canvas.webkitRequestFullscreen(); | |
} | |
} else { | |
if(document.cancelFullScreen) { | |
document.cancelFullScreen(); | |
} else if(document.mozCancelFullScreen) { | |
document.mozCancelFullScreen(); | |
} else if(document.webkitCancelFullScreen) { | |
document.webkitCancelFullScreen(); | |
} | |
} | |
} | |
function handleMouseDown(event) { | |
startDragX = event.clientX; | |
startDragY = event.clientY; | |
currentDragX = event.clientX; | |
currentDragY = event.clientY; | |
currentlyDragging = true; | |
return false; | |
} | |
function handleMouseUp(event) { | |
currentlyDragging = false; | |
return false; | |
} | |
function handleMouseMove(event) { | |
if(currentlyDragging) { | |
currentDragX = event.clientX; | |
currentDragY = event.clientY; | |
} | |
return false; | |
} | |
function handleMouse() { | |
if (currentlyDragging) { | |
xSpeed = (currentDragX - startDragX) / 100; | |
ySpeed = (currentDragY - startDragY) / 100; | |
}else { | |
xSpeed *= 0.8; | |
ySpeed *= 0.8; | |
} | |
yaw += xSpeed; | |
pitch += ySpeed; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment