Taking the canvas to another dimension
Tuesday, 13. November 2007, 16:38:34
Recently Opera published an experimental build on Opera Labs, with support for the video element and video in SVG. This build also includes an experimental addition to the canvas element, the 3d canvas. In order to view the demos presented here you will need to get the Opera Labs build. The build is currently only available for Windows. Mac and Linux versions should be available soon.
Since this is my first post I guess I should introduce myself. My name is Tim Johansson, and I am a core technology developer at Opera Software. I am responsible for, among other things, canvas (including the underlying vector graphics library) and image decoding.
A new addition to HTML5 is the canvas. The canvas is more or less a bitmap that you can draw on using JavaScript. The actual drawing is done by a (rendering) context. The specification includes a 2d context that must be implemented, but also allows browser vendors to add their own contexts. When I first implemented the canvas tag in Opera I though it would be cool to have a 3d context, so I added one.
In this post I will describe the 3d context I added, which is available in the recently released Opera Labs build. The context is called opera-3d and is basically Opera's version of the 3d canvas. For those of you not familiar with the canvas tag here is a crash course in using it.
- Add a <canvas></canvas> tag to your page
- Get the canvas element from a JavaScript using
getElementByIdor something similar. - Call
canvas.getContext(<name>);to get the context
The opera-3d context
Opera's 3d context, unlike Mozilla's, is not a straight mapping to OpenGL. We keep it on a more abstract level. The main reasons for doing this are:
- It makes it easier to implement on non-OpenGL platforms (such as D3D)
- We wanted to have some form of collision detection available
The main concept is that you work with 3D models. You create 3D models, add vertices and triangles to them and finally render them to the canvas. This is what the interface looks like.
interface CanvasRenderingContextOpera3D {
// state
void save(); // push state on state stack
void restore(); // pop state stack and restore state
// scene/frame
void beginScene(); // start rendering a new frame
void endScene(); // finish rendering of the scene and present the result
// transformations
void translate(in float x, in float y, in float z);
void scale(in float x, in float y, in float z);
void rotateX(in float rotation);
void rotateY(in float rotation);
void rotateZ(in float rotation);
// rendering operation
void drawTriangle(in float x1, in float y1, in float z1, in float tex_s1, in float tex_t1,
in float x2, in float y2, in float z2, in float tex_s2, in float tex_t2,
in float x3, in float y3, in float z3, in float tex_s3, in float tex_t3);
void draw3DModel(in Canvas3DModel model);
// create objects
CanvasTexture createTexture(in Image img);
Canvas3DModel create3DModel();
// collision detection
string checkIntersection(in float x, in float y, in float z, in float radius, in Canvas3DModel model);
// rendering state
attribute CanvasTexture texture; // current texture or null for no texture, default is null
attribute string color; // current color, default is transparent black
attribute float fov; // field of view of the scene in degrees, default is 45
attribute float nearPlane; // distance to the near clipping plane, default is 0.1
attribute float farPlane; // distance to the far clipping plane, default is 100
attribute string ztest; // "none", "less", "lessequal", "greater", "greaterequal", "equal", "notequal". Default is "lessequal"
attribute string blend; // "replace", "add", "srcalpha", "multiply". Default is "replace"
};
interface Canvas3DModel {
void addVertex(in float x, in float y, in float z, in float s, in float t);
void addTriangle(in integer vertex1, in integer vertex2, in integer vertex3);
};
interface CanvasTexture{
};
Let's go through the different functions in the order they appear above:
- The
saveandrestorefunctions save and restore the current rendering state. They are very similar tosaveandrestorein the 2D context. - The
translate,scaleandrotatefunctions modify the transformation matrix. The current transformation matrix will transform all vertices rendered with the 3D canvas. This includesCanvas3DModelobjects. beginSceneandendSceneare used to distinguish a frame. The canvas is only updated whenendSceneis called. When it is, the rendered image is copied to the canvas. Only the commands issued betweenbeginSceneandendSceneare drawn to the canvas.drawTriangledraws a single triangle. This method is usually slow and should not be used for rendering a lot of triangles.draw3DModelrenders a model previously created withcreate3DModelto the canvas. This function is much better suited for rendering large batches of triangles.createTexturecreates a texture object from an image object. This method will fail if the image object's dimensions are not powers of two (1, 2, 4, 8, 16, 32 etc.) As with the regular canvas you can create textures from images (including SVG) or other canvases.create3DModelcreates a3DModelobject that can be built (by adding vertices and triangles) and rendered by the script.checkIntersectionis a simple sphere/model collision detection function. The parameters are the sphere (centre and radius) and the model to check for collisions with the sphere. The function returns the collision point as a string when a collision occurs (the point of deepest penetration is coosen as collision point). If no collision was found the function returns an empty string instead.
Example - a rotating cube
This is the first example ever written for the opera-3d context. It creates a model, adds vertices and triangles for a cube and then renders it with different transforms. If you are using an Opera build with 3d canvas enabled you can also see the rotating cube in action. The files used for this example are the HTML file shown below and an image to use as the texture (operalogo.png in this case).
<canvas id="canvas" width="200" height="200">
Canvas not supported!
</canvas>
<script>
var canvas;
var context3d;
var rotation;
var texture;
var cube;
function render(){
context3d.beginScene();
context3d.translate(0,0,-5);
context3d.rotateY(rotation);
context3d.rotateX(rotation);
rotation += 2;
context3d.color = "white";
context3d.draw3DModel(cube);
context3d.endScene();
}
function onTick(){
render();
}
function onload(){
canvas = document.getElementById("canvas");
context3d = canvas.getContext("opera-3d");
if (!context3d)
{
alert("3d canvas not supported");
return;
}
logo = new Image();
logo.src = "operalogo.png";
texture = context3d.createTexture(logo);
context3d.texture = texture;
cube = context3d.create3DModel();
cube.addVertex(-1, 1, 1, 0, 0);
cube.addVertex(1, 1, 1, 1, 0);
cube.addVertex(-1, -1, 1, 0, 1);
cube.addVertex(1, -1, 1, 1, 1);
cube.addVertex(-1, 1, -1, 1, 1);
cube.addVertex(1, 1, -1, 0, 1);
cube.addVertex(-1, -1, -1, 1, 0);
cube.addVertex(1, -1, -1, 0, 0);
cube.addTriangle(0,1,2);
cube.addTriangle(2,1,3);
cube.addTriangle(4,5,6);
cube.addTriangle(6,5,7);
cube.addTriangle(0,4,2);
cube.addTriangle(2,4,6);
cube.addTriangle(1,5,3);
cube.addTriangle(3,5,7);
cube.addTriangle(0,4,1);
cube.addTriangle(1,4,5);
cube.addTriangle(2,6,3);
cube.addTriangle(3,6,7);
setInterval(onTick, 10);
}
document.onload = onload();
</script>
More advanced techniques
In the example above a plain textured cube, which was hard-coded in the script, was rendered. It is possible to do much more than this using the opera-3d context. Below I will describe some techniques that can be used to make more advanced examples.
DOM3 Load and save
Hard-coding models is fine for small objects, but as the
objects grow it becomes more and more difficult to hard-code
them in the script. It is possible to get around this
by converting the models to an XML format and then loading them
into the script using DOM3 load and save to parse the XML.
Here is a modified version of the rotating cube.
Lightmapping
Lightmapping is one of the most famous lighting techniques. It is used in many popular games, for example the quake series. The principle is that you multiply each rendered pixel with the light value at that pixel. The light value for each pixel is pre-calculated and stored in a texture.
The opera-3d context does not have multi-texturing yet, so it
is not possible to do lightmapping in one step, but you can
achieve this effect by doing multi pass
rendering. In the first pass the scene is rendered as usual.
In the second pass ztest is set to equal and blend is set to
multiply. The scene is now rendered with the lightmap instead
of the textures and the result is a lightmapped scene.
Summary
That's it! This article has given you an introduction to the fundamentals of using the Opera 3d canvas. After reading all of this you should know enough to create some cool 3d-canvas demos. If you want to see a more advanced example you can have a look at the 3d snake implementation done by Mathieu 'p01' HENRI. I'm looking forward to seeing all the cool demos people will make! Get in touch with us to share your creations.