WebGL Lesson 10 – loading a world, and the most basic kind of camera

<< Lesson 9Lesson 11 >>

Welcome to my number ten in my series of WebGL tutorials, based on number 10 in the NeHe OpenGL tutorials. In it, we’ll load up a 3D scene from a file (meaning that we could easily switch the file out and extend the demo), and write some simple code so that we can move around within it — a kind of nano-Doom, with its own WAD file format :-)

Here’s what the lesson looks like when run on a browser that supports WebGL:

Click here and you’ll see the live WebGL version, if you’ve got a browser that supports it; here’s how to get one if you don’t. You should find yourself in a room with walls made up of photos of Lionel Brits, who wrote the original OpenGL tutorial on which this is based :-) You can run around the room, and outside it, using the cursor keys or WASD, and you can look up or down using Page Up and Page Down. One thing to look out for is that your viewpoint goes up and down in a “jogging” kind of manner as you run, just for a bit more realism.

More on how it all works below…

The usual warning: these lessons are targeted at people with a reasonable amount of programming knowledge, but no real experience in 3D graphics; the aim is to get you up and running, with a good understanding of what’s going on in the code, so that you can start producing your own 3D Web pages as quickly as possible. If you haven’t read the previous tutorials already, you should probably do so before reading this one — here I will only explain the new stuff.

There may be bugs and misconceptions in this tutorial. If you spot anything wrong, let me know in the comments and I’ll correct it ASAP.

There are two ways you can get the code for this example; just “View Source” while you’re looking at the live version, or if you use GitHub, you can clone it (and the other lessons) from the repository there. A brief warning — because this tutorial loads up the details of the scene it’s showing from a separate file, it will not run in Chrome if you load it up as a file from your local machine. This is a Chrome security “feature”. So if you want to view a version on your own machine, you should either use Firefox beta or Minefield, or install a web server on your machine and view the page through that.

Just as with the last few lessons, the easiest way to explain what’s going on in this page is to start from the bottom and work our way up. Let’s start off with the HTML code inside the body tags at the bottom, which (for the first time since lesson 1!) has something interesting in it:

<body onload="webGLStart();">
<a href="http://learningwebgl.com/blog/?p=1067">&lt;&lt; Back to Lesson 10</a><br />

  <canvas id="lesson10-canvas" style="border: none;" width="500" height="500"></canvas>

  <div id="loadingtext">Loading world...</div>

  <br/>
Use the cursor keys or WASD to run around, and <code>Page Up</code>/<code>Page Down</code> to
look up and down.

<br/>
<br/>
<a href="http://learningwebgl.com/blog/?p=1067">&lt;&lt; Back to Lesson 10</a>

</body>

So, we have a HTML DIV that holds some placeholder text to display while the world that we’re going to display is loading; if the connection between my server and your machine was slow when you loaded the demo above, you might have seen it. However, the message appeared on top of the canvas, not underneath it as you would expect from the HTML. This movement was managed by a bit of CSS code further up, just at the end of the HTML head:

<style type="text/css">
    #loadingtext {
        position:absolute;
        top:250px;
        left:150px;
        font-size:2em;
        color: white;
    }
</style>

So, that’s the HTML. Now let’s take a look at the JavaScript.

The first thing to see is a simple change to our standard webGLStart function; as well as the usual setup stuff, it calls a new function to load the world from the server:

  function webGLStart() {
    var canvas = document.getElementById("lesson10-canvas");
    initGL(canvas);
    initShaders();
    initTexture();
    loadWorld();

    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.enable(gl.DEPTH_TEST);

    document.onkeydown = handleKeyDown;
    document.onkeyup = handleKeyUp;

    tick();
  }

Let’s jump up to that code now; the loadWorld function is just above drawScene, about three quarters of way through the file. It looks like this:

  function loadWorld() {
    var request = new XMLHttpRequest();
    request.open("GET", "world.txt");
    request.onreadystatechange = function() {
      if (request.readyState == 4) {
        handleLoadedWorld(request.responseText);
      }
    }
    request.send();
  }

This style of code may well look familiar; it’s very similar to the kind of thing we used to load the textures. We create an XMLHttpRequest object, which will handle all of the loading, and tell it to use an HTTP GET request to get the file called world.txt from the same directory on the same server as the current page. We specify a callback function to be updated at different stages of the download, and this in turn calls a handleLoadedWorld when the XMLHttpRequest reports that its readyState is 4, which happens when the file has been completely loaded. Once all this has been set up, we tell the XMLHttpRequest to start the process of getting the file by calling its send method.

So, let’s move on to handleLoadedWorld, which is just above loadWorld.

  var worldVertexPositionBuffer = null;
  var worldVertexTextureCoordBuffer = null;
  function handleLoadedWorld(data) {

The function’s job is to parse the contents of the loaded file and use them to create two buffers of the kind we’ve seen so much of in previous lessons. The contents of the loaded file are passed in as a string parameter called data, and the first bit of the code simply parses it. The format of the file we’re using for this example is very simple; it contains a list of triangles, each specified by three vertices. Each vertex is on a line to itself, containing five values: its X, Y and Z coordinates, and its S and T texture coordinates. The file also contains comments (lines starting with //), and blank lines, both of which are ignored, and there is a line at the top that specifies the total number of triangles (though we don’t actually use this).

Now, is this a terribly good file format? Well, actually, no — it’s pretty terrible! It omits a lot of information that we’d like to put into a real scene; for example, normals, or different textures for different objects. In a real-world example, you would use a different format, or even JSON. I’ve stuck with this format, however, because (a) it’s the one used by the original OpenGL lesson and (b) it’s nice and simple to parse. However, having said all that, I’ll not explain the parsing code in detail. Here it is:

    var lines = data.split("\n");
    var vertexCount = 0;
    var vertexPositions = [];
    var vertexTextureCoords = [];
    for (var i in lines) {
      var vals = lines[i].replace(/^\s+/, "").split(/\s+/);
      if (vals.length == 5 && vals[0] != "//") {
        // It is a line describing a vertex; get X, Y and Z first
        vertexPositions.push(parseFloat(vals[0]));
        vertexPositions.push(parseFloat(vals[1]));
        vertexPositions.push(parseFloat(vals[2]));

        // And then the texture coords
        vertexTextureCoords.push(parseFloat(vals[3]));
        vertexTextureCoords.push(parseFloat(vals[4]));

        vertexCount += 1;
      }
    }

At the end of the day, all this really does is take all of the lines with five space-separated values and assume that they contain vertices, and builds up vertexPositions and vertexTextureCoords arrays using them. It also keeps a count of vertices in vertexCount.

The next bit of code should be very familiar-looking by now:

    worldVertexPositionBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexPositionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexPositions), gl.STATIC_DRAW);
    worldVertexPositionBuffer.itemSize = 3;
    worldVertexPositionBuffer.numItems = vertexCount;

    worldVertexTextureCoordBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexTextureCoordBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertexTextureCoords), gl.STATIC_DRAW);
    worldVertexTextureCoordBuffer.itemSize = 2;
    worldVertexTextureCoordBuffer.numItems = vertexCount;

So, we create two buffers containing the vertex details we’ve loaded. Finally, once all that’s done, we clear out the DIV in the HTML that was showing the words “Loading World…”:

    document.getElementById("loadingtext").textContent = "";
}

That’s all the code required to load the world from a file. Before we go on to look at the code that actually uses it, let’s stop briefly and look at something interesting in the world.txt file. The first three vertices, describing the first triangle in the scene, look like this:

// Floor 1
-3.0  0.0 -3.0 0.0 6.0
-3.0  0.0  3.0 0.0 0.0
 3.0  0.0  3.0 6.0 0.0

Remember, thats X, Y, Z, S, T, where S and T are the texture coordinates. You can see that the texture coordinates are between 0 and 6. But I previously said that texture coordinates range from 0 to 1. What’s going on? The answer is that when you ask for a point in a texture, the S and T coordinates are automatically taken modulo one, so 5.5 is taken from the same point in the texture image as 0.5. This means that the texture is, in effect, automatically tiled so that it repeats as many times as required to fill the triangle. That’s obviously very useful when you have a small texture that you want to use on a large object — say, a brickwork texture to cover a wall.

Right, let’s move on to the next interesting bit of code: drawScene. The first thing it does is check whether or not the buffers that are created when we have finished loading the world have been loaded; if they have not, it bails out:

  function drawScene() {
    gl.viewport(0, 0, gl.viewportWidth, gl.viewportHeight);
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

    if (worldVertexTextureCoordBuffer == null || worldVertexPositionBuffer == null) {
      return;
    }

If we do have the buffers set up, the next step is to do our usual setup for the projection and the model-view matrices:

    mat4.perspective(45, gl.viewportWidth / gl.viewportHeight, 0.1, 100.0, pMatrix);

    mat4.identity(mvMatrix);

The next step is to handle our camera — that is, to allow for the fact that we want our viewpoint to move through the scene. The first thing to remember is that, like so many things, WebGL doesn’t support cameras directly, but simulating one is easy enough. If we had a camera, for this simple example we would want to be able to say that it was positioned at a particular X, Y, Z coordinate, and had a certain tilt around the X axis from looking upwards or downwards (called pitch) and a certain angle around the Y axis from having turned left or right (called yaw). Because we can’t change the position of the camera — which is always effectively at (0, 0, 0) looking straight down the Z axis, what we want to do is somehow tell WebGL to adjust the scene that we have to draw, which is specified using X, Y, Z coordinates in what we call world space, into a new frame of reference based on the position and rotation of the camera, which we call eye space.

A simple example might help here. Let’s imagine that we have a really simple scene, in which there is a cube with its centre at (1, 2, 3) in world space. Nothing else. We want to simulate a camera that is positioned at (0, 0, 7), facing down the Z axis, with no pitch or yaw. To do that, we transform the world space coordinates, and we wind up with eye space coordinates for the centre of the cube of (1, 2, -4). Rotations complicate things a little, but not much.

It’s probably pretty clear that this is yet another case for using matrices; and we could in fact keep a “camera matrix” which represents the camera’s position and rotation. But for this example we can keep it even simpler. We can just use our existing model-view matrix.

It turns out (and may well be intuitively obvious by extrapolating from the example above) that we can simulate a camera by “backing out” of the scene in the opposite direction to the way we would move if we were going to the position and rotation of the camera, and then drawing the scene using our usual relative coordinate system. If we imagine ourselves as the camera, we would position ourselves by moving to the appropriate position, then rotating appropriately. So, to “back out”, we undo the rotation, and then undo the move.

Putting it more mathemetically, we can simulate a camera that is at the position (x, y, z) and rotated by a yaw of ψ and a pitch of θ by first rotating by around the X axis, then by around the Y axis, and then by moving to (-x, -y, -z). Once that’s done, we’ve put the model-view matrix in a state such that everything drawn from then on can use world coordinates, and it will get automatically transformed to eye coordinates by the magic of our matrix multiplication in the vertex shader.

There are other ways of positioning cameras, and we’ll go over them in later lessons. For now, here’s the code for this one:

    mat4.rotate(mvMatrix, degToRad(-pitch), [1, 0, 0]);
    mat4.rotate(mvMatrix, degToRad(-yaw), [0, 1, 0]);
    mat4.translate(mvMatrix, [-xPos, -yPos, -zPos]);

Right, once that’s done, all we need to do is draw the scene as described in the buffers that were loaded up earlier. Here’s the code, it should be pretty familiar from previous lessons:

    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, mudTexture);
    gl.uniform1i(shaderProgram.samplerUniform, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexTextureCoordBuffer);
    gl.vertexAttribPointer(shaderProgram.textureCoordAttribute, worldVertexTextureCoordBuffer.itemSize, gl.FLOAT, false, 0, 0);

    gl.bindBuffer(gl.ARRAY_BUFFER, worldVertexPositionBuffer);
    gl.vertexAttribPointer(shaderProgram.vertexPositionAttribute, worldVertexPositionBuffer.itemSize, gl.FLOAT, false, 0, 0);

    setMatrixUniforms();
    gl.drawArrays(gl.TRIANGLES, 0, worldVertexPositionBuffer.numItems);
  }

So, now we’ve covered the bulk of the new code in this lesson. The final bit is the code we use to control our movement, including the “jogging” movement as you run. As in the previous lessons, keyboard actions in this page are designed to give everyone the same rate of movement, however fast or slow their machine. Owners of faster machines get the benefit of a better frame rate, not faster movement!

The way this works is that in our handleKeys function, we use the set of keys currently being pressed by the user to work out a speed — that is, a rate of change of position — a rate of change of the pitch, and a rate of change of the yaw. These will all be zero if no keys are being pressed, or they will be set to fixed values, in units per millisecond, if appropriate keys are being pressed. Here’s the code, which you’ll find about two thirds of the way through the file:

  var pitch = 0;
  var pitchRate = 0;

  var yaw = 0;
  var yawRate = 0;

  var xPos = 0;
  var yPos = 0.4;
  var zPos = 0;

  var speed = 0;

  function handleKeys() {
    if (currentlyPressedKeys[33]) {
      // Page Up
      pitchRate = 0.1;
    } else if (currentlyPressedKeys[34]) {
      // Page Down
      pitchRate = -0.1;
    } else {
      pitchRate = 0;
    }

    if (currentlyPressedKeys[37] || currentlyPressedKeys[65]) {
      // Left cursor key or A
      yawRate = 0.1;
    } else if (currentlyPressedKeys[39] || currentlyPressedKeys[68]) {
      // Right cursor key or D
      yawRate = -0.1;
    } else {
      yawRate = 0;
    }

    if (currentlyPressedKeys[38] || currentlyPressedKeys[87]) {
      // Up cursor key or W
      speed = 0.003;
    } else if (currentlyPressedKeys[40] || currentlyPressedKeys[83]) {
      // Down cursor key
      speed = -0.003;
    } else {
      speed = 0;
    }

  }

So, taking an example from the above, if the left cursor key is pressed then our yawRate is set to 0.1°/ms, which is 100°/s — or, in other words, we start spinning to the left at a rate of one revolution every 3.6 seconds.

These rate-of-change values are, as you’d expect from the previous lessons, used in the animate function to set the values of xPos and zPos, as well as the yaw and the pitch. yPos is also set in animate, but by slightly different logic. Let’s take a look at all of that; you can see the code in the file just below drawScene, near the bottom. Here are the first few lines:

  var lastTime = 0;
// Used to make us "jog" up and down as we move forward.
  var joggingAngle = 0;
  function animate() {
    var timeNow = new Date().getTime();
    if (lastTime != 0) {
      var elapsed = timeNow - lastTime;

Most of that is our normal code to work out the number of milliseconds elapsed from the last time animate was called. joggingAngle is more interesting. The way we get our jogging effect as we move around is by making our Y position follow a sine wave about a central value at “head height” whenever we are not at rest. joggingAngle is the angle we’re pushing into the sine function in order to get our current position.

Let’s look at the code that does that, and also adjusts x and z to allow for our movement:

      if (speed != 0) {
        xPos -= Math.sin(degToRad(yaw)) * speed * elapsed;
        zPos -= Math.cos(degToRad(yaw)) * speed * elapsed;

        joggingAngle += elapsed * 0.6;  // 0.6 "fiddle factor" -- makes it feel more realistic :-)
        yPos = Math.sin(degToRad(joggingAngle)) / 20 + 0.4
      }

Obviously changing position and the jogging effect should only take place when we’re actually moving — so if the speed is not zero, xPos and zPos are adjusted by the current speed by using a simple bit of trigonometry (using the degToRad function to adjust from degrees, which we use for readability, to radians, which the JavaScript trigonometry functions use). Next, joggingAngle is moved on and used to work out our current yPos. All of the numbers we’re using are multiplied by the number of milliseconds since the last call, which makes perfect sense in the case of speed, it being already in terms of units per millisecond. The 0.6 by which we’re adjusting the elapsed number of milliseconds for the joggingAngle is just something determined by trial and error to have a nice, realistic effect.

Once that’s done, we need to adjust yaw and pitch by their respective rates of change, which can be done even when we’re not moving:

      yaw += yawRate * elapsed;
      pitch += pitchRate * elapsed;

Once that’s done, we just need to record the current time so that we can work out the elapsed number of milliseconds next time animate is called, and we’re done:

    }
    lastTime = timeNow;
  }

And that’s it! Now you know all there is to learn from this lesson: a simple way to load up a scene graph from a text file, and one way to implement a camera.

The next lesson will show how to display a sphere, and rotate it using mouse events, and will also explain how you can use rotation matrices to avoid an irritating problem called gimbal-lock.

<< Lesson 9Lesson 11 >>

Acknowledgments: The original version of this tutorial was written by Lionel Brits aka βetelgeuse, and was published on NeHe’s site. This Stack Overflow question showed me how to load a text file up from JavaScript, and this page helped me debug the resulting code.

You can leave a response, or trackback from your own site.

78 Responses to “WebGL Lesson 10 – loading a world, and the most basic kind of camera”

  1. murphy says:

    For the splitting of input lines, you could also use .split(/ +/) to avoid getting empty tokes.

  2. murphy says:

    You wrote “mvRotate(pitch, [1, 0, 0])” in you article for the camera magic code, which lacks a minus. The code is correct though.

  3. Александр Витальевич says:

    Моя тематика . 90 руб.за 1000 знаков.

  4. giles says:

    @murphy — Thanks! I didn’t realise you could use regexes there. I’ve made the change you suggest, though I also had to use a replace() to get rid of leading spaces. Thanks also re: the mvRotate typo; I’ve fixed it.

    @Aleksandr — the person who’s asking for translation is not me, you should post your prices on the free-lance.ru site.

  5. Niavlys says:

    I love this idea, but I hope things will be faster at the end. I’m using Firefox 3.7a1pre (Minefield) right now and these examples are horribly slow, except for the first ones… Maybe this is because of software rendering.

  6. giles says:

    Thanks! The slowness could well be software rendering — on my laptop, which has an Intel graphics card for which I can’t find drivers to support OpenGL 2.0 (which is needed for WebGL to run with hardware acceleration), I see everything slow right down as soon as the page uses textures, which sounds similar to what you see. On my desktop machine, which does support hardware rendering, all of my own demos are fine (though some of the really hard-core GPU-intensive stuff I’ve seen be people like IQ can get a bit slow…)

  7. Niavlys says:

    Now testing with Firefox 3.7a1pre with Ubuntu, under which OpenGl is present, and almost everything works perfectly. I’m pretty much impressed this time :)

  8. giles says:

    Excellent! Glad you like them :-)

  9. Shy says:

    seem to have some HTML mixup at the top there.

  10. giles says:

    Sorry, can’t see that — where’s the problem?

  11. Milos says:

    I found fuction makeLookAt inglUtil.js which should be work same as gluLookAt in openGL.
    I can’t figure out how to change position of viewer and his direction by using this function. Ok, maybe this funciton isn’t key for my problem. I want to put viewer on (0,10,0) and viewer should looking in direction of y axe. How to fix this?

  12. giles says:

    OK, remember that the trick is to start off by moving to the opposite of your camera’s position, and then to draw everything from then on using relative positions. In order to position a camera at (0, 10, 0), you’d want to move up by 10 units, and then rotate around the X axis by +90 degrees. So, you start off your drawScene function by doing that in reverse:

        mvRotate(-90, [1, 0, 0]);
        mvTranslate([0, -10, 0]);
    

    Next, you want to draw your scene. So let’s say you wanted to draw one object at (1, 2, 3) and another at (4, 5, 6). You would do this:

        mvPushMatrix();
        mvTranslate([1, 2, 3]);
        // Draw first object
        mvPopMatrix();
    
        mvPushMatrix();
        mvTranslate([4, 5, 6]);
        // Draw first object
        mvPopMatrix();
    

    Does that help?

  13. meriadeg says:

    Hi, I want to make the rotation with the mouse instead of doing it with the arrows of the keyboard.

    Do you know where I can find a source code withe that, or how I can do that ?

    I know that is possible, beacause Quake 2 and 3 show it. But the source code isn”t very lisible, and the rotation effect is too fast.

  14. meriadeg says:

    And I try to load the world.txt with the source code but without success. The chromium java console tell me it can’t send the xml request.

  15. Adam West says:

    Hi giles,

    first: thanks for your effort to share your knowledge. Meriadeg (2nd above) got a interesting question btw.

    1) For me it would be interesting to know how you would implement the camera into a scene which loads a .json-file. I guess its pretty much the same but a tutorial or a hint would be nice as I am no proger (yet)

    2) Are there any WebGL-Tutorials written for guys who are not familiar to JavaScript and Stuff – not the other way around?

    Thanks for advice

    AW

  16. giles says:

    @meriadeg — check out the mousemove JavaScript event, that should do the trick. Re: loading the world.txt from a file — that’s a security restriction in Chromium, it won’t do XmlHttpRequests from files. The only real solutions are to use Minefield or to set up a local web server for development.

    @Adam West — in lesson 14 I show how to load a JSON file, that might help. I try not to assume any JavaScript knowledge in each of my tutorials beyond what I’ve explained in the previous ones… but I might be rushing ahead a bit. Unfortunately I don’t know of any tutorials that assume less knowledge.

  17. ila says:

    Hi giles:

    I find a easy way to solve the XHR problem in Chromium

    Change:
    function loadWorld() {
    var request = new XMLHttpRequest();
    request.open(“GET”, “world.txt”);
    request.onreadystatechange = function() {
    if (request.readyState == 4) {
    handleLoadedWorld(request.responseText);
    }
    }
    request.send();
    }

    To:
    function loadWorld() {
    var str=”";
    str += “\n”;
    str += “NUMPOLLIES 36\n”;
    str += “\n”;
    str += “// Floor 1\n”;
    str += “-3.0 0.0 -3.0 0.0 6.0\n”;
    str += “-3.0 0.0 3.0 0.0 0.0\n”;
    str += ” 3.0 0.0 3.0 6.0 0.0\n”;
    str += “\n”;
    str += “-3.0 0.0 -3.0 0.0 6.0\n”;
    str += ” 3.0 0.0 -3.0 6.0 6.0\n”;
    str += ” 3.0 0.0 3.0 6.0 0.0\n”;
    str += “\n”;
    str += “// Ceiling 1\n”;
    str += “-3.0 1.0 -3.0 0.0 6.0\n”;
    str += “-3.0 1.0 3.0 0.0 0.0\n”;
    str += ” 3.0 1.0 3.0 6.0 0.0\n”;
    str += “-3.0 1.0 -3.0 0.0 6.0\n”;
    str += ” 3.0 1.0 -3.0 6.0 6.0\n”;
    str += ” 3.0 1.0 3.0 6.0 0.0\n”;
    str += “\n”;
    str += “// A1\n”;
    str += “\n”;
    str += “-2.0 1.0 -2.0 0.0 1.0\n”;
    str += “-2.0 0.0 -2.0 0.0 0.0\n”;
    str += “-0.5 0.0 -2.0 1.5 0.0\n”;
    str += “-2.0 1.0 -2.0 0.0 1.0\n”;
    str += “-0.5 1.0 -2.0 1.5 1.0\n”;
    str += “-0.5 0.0 -2.0 1.5 0.0\n”;
    str += “\n”;
    str += “// A2\n”;
    str += “\n”;
    str += ” 2.0 1.0 -2.0 2.0 1.0\n”;
    str += ” 2.0 0.0 -2.0 2.0 0.0\n”;
    str += ” 0.5 0.0 -2.0 0.5 0.0\n”;
    str += ” 2.0 1.0 -2.0 2.0 1.0\n”;
    str += ” 0.5 1.0 -2.0 0.5 1.0\n”;
    str += ” 0.5 0.0 -2.0 0.5 0.0\n”;
    str += “\n”;
    str += “// B1\n”;
    str += “\n”;
    str += “-2.0 1.0 2.0 2.0 1.0\n”;
    str += “-2.0 0.0 2.0 2.0 0.0\n”;
    str += “-0.5 0.0 2.0 0.5 0.0\n”;
    str += “-2.0 1.0 2.0 2.0 1.0\n”;
    str += “-0.5 1.0 2.0 0.5 1.0\n”;
    str += “-0.5 0.0 2.0 0.5 0.0\n”;
    str += “\n”;
    str += “// B2\n”;
    str += “\n”;
    str += ” 2.0 1.0 2.0 2.0 1.0\n”;
    str += ” 2.0 0.0 2.0 2.0 0.0\n”;
    str += ” 0.5 0.0 2.0 0.5 0.0\n”;
    str += ” 2.0 1.0 2.0 2.0 1.0\n”;
    str += ” 0.5 1.0 2.0 0.5 1.0\n”;
    str += ” 0.5 0.0 2.0 0.5 0.0\n”;
    str += “\n”;
    str += “// C1\n”;
    str += “\n”;
    str += “-2.0 1.0 -2.0 0.0 1.0\n”;
    str += “-2.0 0.0 -2.0 0.0 0.0\n”;
    str += “-2.0 0.0 -0.5 1.5 0.0\n”;
    str += “-2.0 1.0 -2.0 0.0 1.0\n”;
    str += “-2.0 1.0 -0.5 1.5 1.0\n”;
    str += “-2.0 0.0 -0.5 1.5 0.0\n”;
    str += “\n”;
    str += “// C2\n”;
    str += “\n”;
    str += “-2.0 1.0 2.0 2.0 1.0\n”;
    str += “-2.0 0.0 2.0 2.0 0.0\n”;
    str += “-2.0 0.0 0.5 0.5 0.0\n”;
    str += “-2.0 1.0 2.0 2.0 1.0\n”;
    str += “-2.0 1.0 0.5 0.5 1.0\n”;
    str += “-2.0 0.0 0.5 0.5 0.0\n”;
    str += “\n”;
    str += “// D1\n”;
    str += “\n”;
    str += “2.0 1.0 -2.0 0.0 1.0\n”;
    str += “2.0 0.0 -2.0 0.0 0.0\n”;
    str += “2.0 0.0 -0.5 1.5 0.0\n”;
    str += “2.0 1.0 -2.0 0.0 1.0\n”;
    str += “2.0 1.0 -0.5 1.5 1.0\n”;
    str += “2.0 0.0 -0.5 1.5 0.0\n”;
    str += “\n”;
    str += “// D2\n”;
    str += “\n”;
    str += “2.0 1.0 2.0 2.0 1.0\n”;
    str += “2.0 0.0 2.0 2.0 0.0\n”;
    str += “2.0 0.0 0.5 0.5 0.0\n”;
    str += “2.0 1.0 2.0 2.0 1.0\n”;
    str += “2.0 1.0 0.5 0.5 1.0\n”;
    str += “2.0 0.0 0.5 0.5 0.0\n”;
    str += “\n”;
    str += “// Upper hallway – L\n”;
    str += “-0.5 1.0 -3.0 0.0 1.0\n”;
    str += “-0.5 0.0 -3.0 0.0 0.0\n”;
    str += “-0.5 0.0 -2.0 1.0 0.0\n”;
    str += “-0.5 1.0 -3.0 0.0 1.0\n”;
    str += “-0.5 1.0 -2.0 1.0 1.0\n”;
    str += “-0.5 0.0 -2.0 1.0 0.0\n”;
    str += “\n”;
    str += “// Upper hallway – R\n”;
    str += “0.5 1.0 -3.0 0.0 1.0\n”;
    str += “0.5 0.0 -3.0 0.0 0.0\n”;
    str += “0.5 0.0 -2.0 1.0 0.0\n”;
    str += “0.5 1.0 -3.0 0.0 1.0\n”;
    str += “0.5 1.0 -2.0 1.0 1.0\n”;
    str += “0.5 0.0 -2.0 1.0 0.0\n”;
    str += “\n”;
    str += “// Lower hallway – L\n”;
    str += “-0.5 1.0 3.0 0.0 1.0\n”;
    str += “-0.5 0.0 3.0 0.0 0.0\n”;
    str += “-0.5 0.0 2.0 1.0 0.0\n”;
    str += “-0.5 1.0 3.0 0.0 1.0\n”;
    str += “-0.5 1.0 2.0 1.0 1.0\n”;
    str += “-0.5 0.0 2.0 1.0 0.0\n”;
    str += “\n”;
    str += “// Lower hallway – R\n”;
    str += “0.5 1.0 3.0 0.0 1.0\n”;
    str += “0.5 0.0 3.0 0.0 0.0\n”;
    str += “0.5 0.0 2.0 1.0 0.0\n”;
    str += “0.5 1.0 3.0 0.0 1.0\n”;
    str += “0.5 1.0 2.0 1.0 1.0\n”;
    str += “0.5 0.0 2.0 1.0 0.0\n”;
    str += “\n”;
    str += “\n”;
    str += “// Left hallway – Lw\n”;
    str += “\n”;
    str += “-3.0 1.0 0.5 1.0 1.0\n”;
    str += “-3.0 0.0 0.5 1.0 0.0\n”;
    str += “-2.0 0.0 0.5 0.0 0.0\n”;
    str += “-3.0 1.0 0.5 1.0 1.0\n”;
    str += “-2.0 1.0 0.5 0.0 1.0\n”;
    str += “-2.0 0.0 0.5 0.0 0.0\n”;
    str += “\n”;
    str += “// Left hallway – Hi\n”;
    str += “\n”;
    str += “-3.0 1.0 -0.5 1.0 1.0\n”;
    str += “-3.0 0.0 -0.5 1.0 0.0\n”;
    str += “-2.0 0.0 -0.5 0.0 0.0\n”;
    str += “-3.0 1.0 -0.5 1.0 1.0\n”;
    str += “-2.0 1.0 -0.5 0.0 1.0\n”;
    str += “-2.0 0.0 -0.5 0.0 0.0\n”;
    str += “\n”;
    str += “// Right hallway – Lw\n”;
    str += “\n”;
    str += “3.0 1.0 0.5 1.0 1.0\n”;
    str += “3.0 0.0 0.5 1.0 0.0\n”;
    str += “2.0 0.0 0.5 0.0 0.0\n”;
    str += “3.0 1.0 0.5 1.0 1.0\n”;
    str += “2.0 1.0 0.5 0.0 1.0\n”;
    str += “2.0 0.0 0.5 0.0 0.0\n”;
    str += “\n”;
    str += “// Right hallway – Hi\n”;
    str += “\n”;
    str += “3.0 1.0 -0.5 1.0 1.0\n”;
    str += “3.0 0.0 -0.5 1.0 0.0\n”;
    str += “2.0 0.0 -0.5 0.0 0.0\n”;
    str += “3.0 1.0 -0.5 1.0 1.0\n”;
    str += “2.0 1.0 -0.5 0.0 1.0\n”;
    str += “2.0 0.0 -0.5 0.0 0.0\n”;
    handleLoadedWorld(str);

    }

    Thanks for your amazing WebGL Tutorials.

  18. Meriadeg says:

    Thanks Giles,

    It’s a good link :)
    Now i’m sure à i have to work hard javascript fort webGl.

    In fact I should wanted a key number like 54 or another number to get the ID of mouse boutton or event. Lol, like there is with any button of the Keyboard.

  19. giles says:

    @ila — right, that would work! Not the best solution if you’re trying to demonstrate how to load stuff from external files, though :-)

    @Meriadeg — glad to help!

  20. najib says:

    Thanks for your work.

  21. Ramon says:

    I’ve a bunch of old codes and some stuff from gametutorials
    about moving in a 3D world using gluLookat

    I’m also a newbie in Javascript I’ve found makeLookAt() at glUtils.
    but how does it work?
    Does anyone have a working version of “makeLookAt” or “LookAt”?

  22. [...] quickly discovered that Lesson 10 involved downloading a small scene and navigating it Doom-style, so I skipped ahead and using it as [...]

  23. Paul says:

    Please, I need help.
    With Chrome, I could run all the lessons. But when I try to download all the page to my computer, I couldn’t run this lesson at local ( though I could run lesson 1->9 at local)

  24. Ramon says:

    Well
    I finally found out how to makeLookAt work.
    http://www.opengl.org/wiki/GluLookAt_code

    thanks man!

  25. Josh says:

    Thank you for the tutorials. Would it be possible to see a version where the camera moves instead of the world moving to mimic a camera.

  26. giles says:

    @Ramon — glad you found a solution, sorry I couldn’t help.

    @Paul — that’s because Chrome won’t let pages loaded from a local filesystem load up other files. Personally, I think that’s a mistake, but it’s not something they’re going to change. You’ll need to install a web server on your machine and look at the lesson through that, or use Minefield. I’ll add a warning to this tutorial so that other people don’t get surprise by this.

    @Josh — that’s the thing, WebGL doesn’t support a camera as such. Instead, everything is drawn in “eye space”, relative to the display. However, it’s so easy to mimic a camera in your own code that this is no great hardship, and it makes WebGL itself simpler, which makes it possible for it to be supported on a larger number of devices.

  27. volts says:

    Hi Giles, Thanks for the lesson’s, I’m finding them very useful. Here is how to get this example to run locally in FireFox 4.0 (but not Chrome) using a technique from http://scriptnode.com/article/multiline-strings-in-javascript/ to load the world data.

    Add the following line after the meta tag

    Download world.txt and rename it world.js. Insert the following line at the top of the file:

    var world = ” + ;

    Then comment out everything in the loadWorld() function, except for the line containing “handleLoadedWorld(world);”.

  28. volts says:

    Hmmm. The XML after ‘var world = ” +’ got stripped out when I submitted my comment. Just follow the E4X example at http://scriptnode.com/article/multiline-strings-in-javascript/.

  29. giles says:

    Wow, that’s clever! I didn’t know JavaScript could do that :-)

  30. gokumc says:

    Hey guys,

    First of all I would like to thank you giles for the excellent lessons. But in this one i got stuck.
    My problem is to run the files locally. As far as I understood volts has found a solution but unfortunately I didn’t get it.
    Changing the format of world from .txt to .js and adding the line ‘var world = “+’ to its top doesn’t really solve the problem.
    Could someone please explain it step by step and for people with less understanding of E4X?

    Thanks in advance.

  31. giles says:

    @gokumc — the problem is basically that Chrome won’t let you load up a file on your local disk from JavaScript, even if the JavaScript itself came from another file on the same disk. So you can work around the problem by using Firefox, or by setting up a web server locally and then accessing the pages from Chrome via that web server. Does that help?

  32. gokumc says:

    Yes it helped, thanks a lot! After some research, I’ve figured out another way to load local files in JavaScript using Chrome. Therefore I had to start Chrome with following arguments: “–allow-file-access-from-files”.
    The reason why it was so important for me to get it work with Chrome is simply that I’m more familiar with its debugging environment.
    And finally it works ;-) (even if this solution causes a security leak).

  33. giles says:

    @gokumc — that’s a useful tip, I didn’t know about that flag — thanks!

  34. [...] lezione di oggi utilizza in parte il codice e le scelte progettuali documentate nel seguente articolo che tratta il medesimo argomento. Approfitto della parentesi per invitarvi a dare un occhio a [...]

  35. [...] next problem I thought of, was how to implement multiple textures. This led me to adapting lesson 10 of the LearningWebGL tutorials. Please find the result [...]

  36. Budha says:

    thanx Giles for the lesson. But i have tried all the work arounds that you have mentioned to run this file from my system. I basically copy pasted the source code and put the the js files in the same folder along with a file for mud.gif and world.txt.
    I tried setting up a local server but the world still doesnt render. It shows the loading… part first and then basically displays a black canvas. that’s it. I have been stuck trying to decipher the problem for a couple of days now. Need your help. Thanx in advanvce.

  37. Budha says:

    this is the temporary link to my website. Can you please suggest why it is not working?

  38. Lighting says:

    Somehow this awesome moving camera “breaks” the directional lighting I implemented from the earlier lesson…

    Let’s say I have a directional light “coming from -Z” ie. “going to +Z”. I then “rotate the avatar / the camera” to the left or right. Light always seems to be “coming from the camera” rather than from the fixed direction it was meant to come from.

    How would we need to apply the camera transformation to the directional light vec3?

    I presume this would need to happen between vec3.normalize and vec3.scale (which I don’t use since I find it more intuitive to specify where the light is coming from –its origin– rather than where it is going to = its direction).

    I tried multiplying the light direction vector with the mvMatrix uniform directly in the shader and other combinations — got all kinds of weird effects but not the realistic one of “light always coming from point xyz no matter the current tilt/yaw of the camera”.

  39. Harco says:

    Thank you Giles for your excellent tutorials!

    @Lighting:
    Since i am quite a newbie to WebGL i am not sure if this is gonna to help you. In my case the direction lighting (done in the vertex shader) in combination with the camera works perfect.

    I use this code snippet to create a matrix which represents the camera:

    var cMatrix = mat4.create();
    mat4.identity(cMatrix);
    mat4.rotate(cMatrix,Util.degToRad(this.pitch), [1, 0, 0]);
    mat4.rotate(cMatrix, Util.degToRad(this.yaw), [0, 1, 0]);
    mat4.translate(cMatrix, [this.posX, this.posY, this.posZ]);
    mat4.inverse(cMatrix);
    program.setUniform(“uCMatrix”, cMatrix);

    In my vertex shader i am doing this:
    gl_Position = uPMatrix * uCMatrix * uMVMatrix * vec4(aVertexPosition, 1.0);

    The lighting calculations are the same as described by Giles.

    It would be great if someone with more knowledge can confirm if this is a correct way to implement a camera with to use of a matrix.

  40. goat says:

    I was toying around with this and was creating my own room with it. Everything was going fine until I tried to change the texture to something else(download new file, change the ‘mudTexture.image.src = “”;’ bit to match the new file). It seems the only textures this code supports are the specific crate.gif and mud.gif files you’ve used. Is there something I missed while reading through these that prevents them from using other .gifs or .jpgs?

  41. giles says:

    Whoops, I’ve obviously been missing comments!

    @Budha — sorry, I guess you’ve moved on by now — the output in the browser’s error console would have helped, I think.

    @Lighting — you’re definitely heading along the right lines. @Harco’s suggestion looks right.

    @goat — are your textures power-of-two — that is, their width and height dimensions are both powers of two, like 128, 256, 512, or similar?

  42. goat says:

    Looking at the pictures, they’re all even numbers and square, but not powers of two. I can understand that being a limit, but I’m not sure why it is with the code provided. Is there any particular bit that limits it as such?

  43. giles says:

    It’s implicit in the settings we’re using for the textures. See http://www.khronos.org/webgl/wiki/WebGL_and_OpenGL_Differences#Non-Power_of_Two_Texture_Support

  44. jgrc says:

    Excellent tutorial.
    But i dont like the method to remove the “loading” div. With colored divs )background color) dont work properly.

    document.getElementById(“loadingtext”).style.display = “none”;

    This line, for example, is more elegant.

  45. [...] tutorial de la serie Aprende webGL. Es una traducción no literal de su respectivo tutorial en Learning webGL, que a su vez está basada en el capítulo 10 del tutorial sobre openGL de NeHe. Hoy aprenderemos a [...]

  46. Mark says:

    @giles – Would you by any chance have a level/map editor that allows you to save the map in “x y z u v” coords, just like the world.txt.

    - Mark

  47. Mark says:

    Hallo,

    At first I want to thank you for your hard work!

    However, does anyone know how I could block the way through the wall?

  48. phil says:

    Hi,

    I love learningwebgl.com, there’s really nothing else like it. I was wondering, how would you do collision detection with the wall? I’ve done it with points, but have no idea how to do it with a plane that is in a buffer.

    Cheers for any help,

    Phil

  49. Rod says:

    Hey Giles,
    First the kudos: awesome work with the tutorial. equally awesome work on answering questions here. I’m truly thankful.
    Now, I’m having a hard time wrapping my head around the titled texture thing. I get the modulo math and all but why exactly 6 on the UVs? Is 6 related to the world model in any way? maybe some ratio or calculation done before hand? In other words, if I was to allow a user to create a rectangle and use a texture to “paint” it. How do I get to the correct UV value to allow the texture to repeat itself throughout the referred rectangle?

    thanks again,
    -rod

  50. tushar says:

    file is not showing anything on FireFox . cn u pls explain the reson behind it…

Leave a Reply

Subscribe to RSS Feed Follow Learning WebGL on Twitter