Thursday, November 18, 2010

Building the VideoSphere - Going Further with WebGLU

In this blog post I will pick apart how my videosphere demo is put together. I suspect the simplicity will surprise some. Once again, viewing these demos requires a WebGL enabled web browser, instructions on how to get one are available on the Khronos site.

Here's a video of the same demo
In addition to exploring how the demo is put together we'll look at some of the different ways to get your data into WebGLU, hopefully now matter how you want to organize your page you'll be able to fit WebGLU into that. First I'll show the most concise version, great if you don't mind WebGLU making XHRs to load its modules, shaders, and material definitions.




Last time we looked at how to build a simple WebGLU app. Let's take this a bit further and add some pizzaz. Specifically, let's see how to take a video, wrap it around the camera, and allow users to manipulate that camera while the video plays around them.

Here's the code.

<html><head>
<script type="text/javascript" src="path/to/webglu.js">
</script>
<style type="text/css"> video { display: none; } </style>
<script type="application/x-javascript">
function start() {
    if ($W.initialize()) {
        $W.useControlProfiles();
        $G.profiles.DragToRotateCamera.apply();
        $G.profiles.ScrollToZoomCamera.apply();

        $W.createObject({
            type     : $W.GL.TRIANGLES,
            material : new $W.Material({path:'video.json'}),
            model    : $W.util.genSphere(50,50)
        });

        var videoPath = $W.util.URLParams['video'];
        if (typeof(videoPath) === 'undefined') {
            videoPath = 'new_york.ogv';
        }
        $W.textures.video.setSource(videoPath);
        $W.textures.video.video.loop = true;
        $W.textures.video.video.play();

        $W.start();
    }
};
</script></head><body onload="start()"> 
<canvas id='canvas' width='800' height='600'></canvas>
</body></html>


Since WebGLU appends a video element to the page we need to hide it from being visible directly.



A new feature of WebGLU is the idea of 'control profiles', these are premade sets of functions that provide some functionality to some set of inputs. Things like 3D world manipulation are going to be pretty similar between applications, so the profiles provide a way to get that interactivity with a fraction of the effort. At the moment only DragToRotateCamera and ScrollToZoomCamera are implemented, but I hope to build a decent set of profiles as time goes on. To load the profiles we need to call $W.useControlProfiles() and then apply the profiles we want to use. Profiles are stored in $G.profiles, $G is the GameGLU object. In a later blog post I'll go into more detail about GameGLU and show how we can use it to easily create custom interaction.
$W.useControlProfiles();
$G.profiles.DragToRotateCamera.apply();
$G.profiles.ScrollToZoomCamera.apply();


Next we need a sphere to map the video onto. $W.createObject() takes a JavaScript object as its argument to allow great flexibility in how to create a WebGLU Object. WebGLU Objects hold the vertex attribute arrays and the material used to render those attributes.
$W.createObject({
    type     : $W.GL.TRIANGLES,
    material : new $W.Material({path:'video.json'}),
    model    : $W.util.genSphere(50,50)
});
In this case we are using the {type, material, model} format. Which takes a WebGL render type like $W.GL.TRIANGLES or $W.GL.LINES, a Material or the name of a Material, and a 'model' which is another object in the format {vertices, normals, texCoords, indices} which are all arrays.

For this demo we used a function provided by WebGLU which generates the data for a sphere in the model format.




We're loading the material from a JSON file, though we can just as easily pass an object literal of the same format to $W.Material() if we want to define something procedurally.

A material needs a name, a shader program and, optionally, some textures. A shader program is made up of shaders, which need a name and a path to a shader file or the shader type and its source code or the id of a script element of type "x-shader/x-vertex" or "x-shader/x-fragment". For this material we'll use the textured shaders WebGLU provides. Note that if a program or shader of the given name already exists it will use that instead.

Here's the contents of video.json:
{
    name: "video",
    program: {
        name: "wglu_textured" ,
          shaders: [
              {name : "textured_vs", 
               path : $W.paths.shaders + "wglu_texture.vert"},

              {name : "textured_fs", 
               path : $W.paths.shaders + "wglu_texture.frag"}
          ] 
        },
    textures: [ {name:"video", type:"video", path:""} ]
}


video.json purposely left the video path unspecified, so we need to set it now. We can use the paramaters that WebGLU helpfully parses from the URL for us and fallback to a video we know exists if none was given.
var videoPath = $W.util.URLParams['video'];
if (typeof(videoPath) === 'undefined') {
    videoPath = 'new_york.ogv';
}
$W.textures.video.setSource(videoPath);


Then we just tell the video to loop when it finishes then start it playing and we're all set to render!
$W.textures.video.video.loop = true;
$W.textures.video.video.play();

$W.start();
If you're in a position where you need to avoid XHRs then WebGLU provides a way to do that. I won't go into too much detail, but here's the same page, but written to avoid XHRs.
<html><head>
<script type="text/javascript" src="path/to/webglu.complete.js">
</script>

<!-- WebGLU normally loads Sylvester (its vector/matrix math lib) via XHR,
if we include it directly in the page it will not do that -->
<script type="text/javascript" src="path/to/Sylvester.js">
</script>

<style type="text/css"> video { display: none; } </style>

<!-- To avoid XHR with the shaders we embed them in script tags and 
reference them by element id -->
<script type="x-shader/x-vertex" id="textured_vs">
attribute vec3 vertex;
attribute vec2 texCoord;

uniform mat4 ProjectionMatrix;
uniform mat4 ModelViewMatrix;

varying vec2 vTexCoord;

void main(void) {
    vTexCoord = texCoord;
    gl_Position = ProjectionMatrix * 
                  ModelViewMatrix  * 
                  vec4(vertex, 1.0);
}  
</script>

<script type="x-shader/x-fragment" id="textured_fs">
#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D wglu_mat_texture0;
varying vec2 vTexCoord;

void main(void) {
    gl_FragColor = texture2D(wglu_mat_texture0, vTexCoord);
} 
</script>

<script type="application/x-javascript">
function start() {
    if ($W.initialize()) {

        $W.useControlProfiles();
        $G.profiles.DragToRotateCamera.apply();
        $G.profiles.ScrollToZoomCamera.apply();

        var videoPath = $W.util.URLParams['video'];
        if (typeof(videoPath) === 'undefined') {
            videoPath = 'new_york.ogv';
        }

        // Declare the Material procedurally so we don't 
        // need to load it from another file
        new $W.Material({
            name: "videoMaterial",

            program: { name: "wglu_textured",
                       shaders: [ 
                           {id:"textured_vs"}, 
                           {id:"textured_fs"} ] },
 
            textures: [ {name:"video", 
                         type:"Video", 
                         path:videoPath} ]
        });

        $W.createObject({
            type     : $W.GL.TRIANGLES,
            material : "videoMaterial",
            model    : $W.util.genSphere(50,50)
        });

        $W.textures.video.video.loop = true;
        $W.textures.video.video.play();

        $W.start();
    }
}

</script> </head>
<body onload="start()">
<canvas id='canvas' width='800' height='600'></canvas><br>
</body> </html>


There you have it, rich 3D apps is very simple if you use WebGLU. That means you have more time to make compelling content instead of wasting it on fiddling with the, esoteric by comparison, WebGL directly.

1 comment:

  1. It doesn't seem to work on latest dev Chrome.
    9.0.597.16 dev

    ReplyDelete