Rozengain - New Media Development Blog

Syndicate content
Updated: 15 min 21 sec ago

Using WebGL & GLSL Shaders to Create a Tunnel Effect

Tue, 08/10/2010 - 14:38

Now here’s something that’s been done a thousand times before in a thousand different ways! Sometimes it’s good to re-invent the wheel so you can learn something new My purpose for this demo was to experiment with vertex and fragment shaders. They’re very powerful tools that allow you to create the most amazing effects.

Click here to see the live demo. If you don’t have a WebGL-enabled browser you can click on the image below to see the YouTube video.

This Wikipedia page has a good explanation about shaders. Simply said, the vertex shader allows you to modify every vertex and the fragment shader (or pixel shader) allows you to play with the pixel colors. Here are the basic steps for this demo.

Create the geometry

I created a cylinder that consists of:

  • vertices: the further away, the smaller the radius
  • indices
  • vertex colors: the vertices that are close to the camera are white, the vertices that are the furthest away are black. These colors are multiplied with the texture in a later stage to give the feeling of depth.
  • texture coordinates
Modifying the geometry with a Vertex Shader

The vertex shader is executed every frame. The cylinder can be bended by using the sine and cosine functions. To animate this a time variable is passed on to the shader.

Creating the illusion of movement

To create the illusion of movement the texture coordinates are displaced in the vertex shader. The time variable is used for this as well. Only the V coordinate is displaced.

Creating the illusion of depth

When the geometry was created the color for each vertex was determined as well. The closest vertices are white and the ones further away change gradually to black. To create depth these colors are multiplied with the texture pixels in the fragment shader.

The GLSL code

Here’s what the code looks like. This time I only show the specific shader code for this demo. Everything else is explained in the brilliant Learning WebGL tutorials (the code for this demo is based on these).

The vertex shader:

<script id="shader-vs" type="x-shader/x-vertex"> // -- "attribute": read-only per-vertex data, available only within vertex shaders. // -- the vertex position (x, y, z) attribute vec3 aVertexPosition; // -- the vertex color (r, g, b, a) attribute vec4 aVertexColor; // -- the texture coordinate for this vertex (u, v) attribute vec2 aTextureCoord; // -- "uniform": remains constant during each shader execution. // -- model-view matrix uniform mat4 uMVMatrix; // -- projection matrix uniform mat4 uPMatrix; // -- the time value (changes every frame) uniform float fTime; // -- "varying": output of the vertex shader that corresponds to read-only interpolated input // of the fragment shader // -- the color varying vec4 vColor; // -- the texture coordinates varying vec2 vTextureCoord; void main(void) { vec3 pos=aVertexPosition; // -- displace the x coordinate based on the time and the z position pos.x += cos(fTime + (aVertexPosition.z/4.0)); // -- displace the y coordinate based on the time and the z position pos.y += sin(fTime + (aVertexPosition.z/4.0)); // -- transform the vertex gl_Position = uPMatrix * uMVMatrix * vec4(pos, 1.0); // -- copy the vertex color vColor = aVertexColor; // -- displace the texture's y (v) coordinate. This gives the illusion of movement. vec2 texcoord=aTextureCoord; texcoord.y = texcoord.y + (fTime); // -- copy the texture coordinate vTextureCoord = texcoord; } </script>

The fragment shader:

<script id="shader-fs" type="x-shader/x-fragment"> #ifdef GL_ES precision highp float; #endif uniform sampler2D uSampler; varying vec4 vColor; varying vec2 vTextureCoord; void main(void) { // -- get the pixel from the texture vec4 textureColor = texture2D(uSampler, vec2(vTextureCoord.s, vTextureCoord.t)); // -- multiply the texture pixel with the vertex color gl_FragColor = vColor * textureColor; } </script>

X3DOM Exploration #2: Creating Geometry with IndexedFaceSet

Fri, 08/06/2010 - 11:57

In my previous post about X3DOM I covered some of the very basics. I used a <Box> object and slapped a Twitter avatar on it. In this post I’m going to use the <IndexedFaceSet> element to create geometry dynamically.

Read them pixels

For this example I’ve used a PHP-script that loads an image, resizes it to an abitrary size, reads every pixel’s color and calculates the average between red, green and blue. This average is used to determine the height of the object. All very simple.

<IndexedFaceSet>

For each pixel we’re going to create a box that’s made up of 8 vertices and 12 faces (triangles). So that 8 (x, y, z) values, 8 (r, g, b) values and 3 * 12 faces = 36 index values. In this example, the <IndexedFaceSet> elements has two children. One for the vertices (<Coordinate>) and one for the colors (<Color>). The <Coordinate> element has an attribute called “point” which we use to specify the (x, y, z) values. These are all separated by spaces:

<Coordinate point="x1 y1 z1 x2 y2 z2 x3 y3 z3 .... etc "/>

These vertices have corresponding colors which are specified in the “color” attribute of the <Color> element:

<Color color="r1 g1 b1 r2 g2 b2 r3 g3 b3 .... etc "/>

The next step is to connect all these vertices to create a shape. This is done by passing the indices to the “coordIndex” attribute of the <IndexedFaceSet> element. This index corresponds to the specific vertex that was defined in the <Coordinate> element.

<IndexedFaceSet coordIndex="face1_index1 face1_index2 face1_index3 -1 face2_index1 face2_index2 face2_index3 -1 .... etc "/>

Note that the “-1″ value separates the individual faces.

Examples

Here are some examples. You can click on them to see the live version.

The Zohan

Jim

Angie

More WebGL Exploration: Visualising Mutual Twitter Friends with X3DOM

Thu, 08/05/2010 - 09:34

After the WebGL BoF at Siggraph I spoke to some people from the Web3D consortium and the Fraunhofer institute. This was quite interesting. The Fraunhofer institute is working on an X3D WebGL implementation called X3DOM. Basically it means that you can set up a 3D scene as part of any HTML5 DOM tree.

This should make hardware accelerated 3D accessible for anyone who’s familiar with HTML and not familiar with WebGL/OpenGL. Most people don’t care about the low-level specifics, they just want to get 3D stuff up and running quickly. X3DOM should enable the users to do so. Before Siggraph I had heard about X3DOM but never took the time to take a closer look at it.

Show Mutual Twitter Friends

For this simple example I decided to grab some Twitter data. I found a web service that returns the mutual friends, followers, etc from two Twitter users. I had to do some PHP scripting to filter the results and I composed the actual XML server-side. I won’t bother you with the details but if you’re interested I can send you the PHP script I’ve been using.

If you want to view the final results before I go into the specifics, follow this link. You can click and drag and use the scroll wheel to navigate in 3D space. If you don’t have a WebGL-enabled browser you can click on the image below to see the YouTube video.

Setting up the scene with X3DOM

An important thing to consider is that the mime type for xhtml based documents should be “application/xhtml+xml”. Using the .xhtml extension should be sufficient for the browser to use this mime type.

An X3DOM document starts with the <X3D> root node. Here you specify the position, the dimensions and you can indicate whether you want so see the stats and the debug log:

<X3D showStat="false" showLog="false" x="0px" y="0px" width="800px" height="600px">

Then a <Scene> and <Viewpoint> are needed:

<Scene> <Viewpoint position='0 0 40'></Viewpoint>

All the individual cubes are enclosed in a <Transform> tag. This is used for scaling, rotating and translating. This tag is also key to animations, which will be shown later on. All objects are defined in a <Shape> tag. In this instance the onclick event handler is attached to this node. Clicking the shape will show the user’s Twitter name. The material properties and texture are enclosed in an <Appearance> tag. Here we specify the texture url, diffuse color, shininess, etc. The <Box> tag adds a box primitive to the scene:

<Transform DEF='MyTransform2' scale='1 1 1' rotation='0 0 0 0' translation='1.3891854213354 7.8784620240977 28.75'> <Shape onclick="showUserInfo('masdennis')"> <Appearance> <ImageTexture url='ResizeImage.php?size=256&url=http://a2.twimg.com/profile_images/1078865146/roasted-pepper.jpg'/> <Material diffuseColor='0.7 0.7 0.7' shininess='1' specularColor='0.95 0.3247 0.3247' /> </Appearance> <Box DEF='box'></Box> </Shape> </Transform>

I’m using a php script to convert the texture to a power of two texture.

All these textured cubes are grouped with a <Group> tag. There are two <Transform> nodes that control the group’s rotation around the x-axis and the rotation around the y-axis:

<Transform DEF="XAxisRotation"> <Transform DEF="YAxisRotation">

Here’s where animation comes into place. To control the animation time-wise we can use a <TimeSensor> node:

<TimeSensor DEF='XTimeInterval' cycleInterval='32' loop='true'/>

The object’s rotation can be transformed by using <OrientationInterpolator>. The “key” attribute specifices the time values and the “keyValue” attribute specificies the rotation in radians. The values are specified in “x y z value” format. So if we want to do a 180 degrees rotation (PI) around the x axis this value can be used: 1 0 0 3.14159265.

<OrientationInterpolator DEF='XSpinner' key='0 0.25 0.5 0.75 1.00' keyValue='1 0 0 0 1 0 0 1.57079633 1 0 0 3.14159265 1 0 0 4.71238898 1 0 0 6.28318531'/>

Next we need to connect the <OrientationInterpolator> to the <Transform>:

<ROUTE fromNode='XSpinner' fromField='value_changed' toNode='XAxisRotation' toField='rotation'/>

The final step involves connecting the <TimeSensor> to the <OrientationInterpolator>:

<ROUTE fromNode='XTimeInterval' fromField='fraction_changed' toNode='XSpinner' toField='set_fraction'/>

It’s as easy as that :-). My first impression of X3DOM is that it’s very easy to work with. Specifying the scene through XML nodes is actually very convenient. Next time I’ll give DOM scripting a try to see how that works.

If you want to know more about X3DOM or want to see more examples, go here.

Speaking at Siggraph 2010

Tue, 07/27/2010 - 20:11

Tomorrow I’m off to Los Angeles do one of the WebGL BoF talks at Siggraph. I’m very excited to be part of this. My talk will be about the opportunities WebGL has to offer to us as a creative agency.
I’ve been working with WebGL a lot lately. A lot of experimentation but also some client work. The ability to work with hardware accelerated graphics on the web has been inspiring.
I will write more about the conference in a few days. Be sure to give me a shout if you’re in LA as well

Visualizing Last.fm data with WebGL, GLGE, jQuery

Fri, 07/23/2010 - 10:47

I’m a jQuery newbie so I thought I’d try it out in combination with WebGL. It makes a great example together with the GLGE library for WebGL.

Mind you, there are better ways to do tunnels in 3D. This demo is meant to demonstrate how specific things work with GLGE.

An explanation of what’s going on can be found below. Click here to see the live demo or watch the YouTube video if you don’t have a WebGL-enabled browser:

Using jQuery to get the data from Last.fm

First there’s a bit of jQuery code that gets the xml and the images from Last.fm. I use a php proxy script to circumvent security restrictions.

var images = []; var apiKey = 'e351ab3a5309170fa0390aeaebd09304'; /** * Last.FM Service Calls */ var serviceCalls = [ 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=napalm%20death&api_key=' + apiKey), 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=metallica&api_key=' + apiKey), 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=ambulette&api_key=' + apiKey), 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=dri&api_key=' + apiKey) ]; /** * Start the Last.FM Service Calls */ $(document).ready(function() { loadNextService(); }); /** * Get the artists's top albums */ function loadNextService() { if(serviceCalls.length == 0) { start3D(); return; } var serviceCall = serviceCalls.shift(); /** * Traverse the xml file and get the images */ $.ajax({ type: "GET", url: serviceCall, dataType: "xml", success: function(xml) { $(xml).find('album').each(function(){ $(this).find('image').each(function(){ var id = $(this).attr('size'); if(id == "large") images.push($(this).text()); }) }); loadNextService(); } }); } Creating the tunnel with GLGE

I’m going to skip the basic stuff (setting up the scene, adding textures) that I explained in a previous post (here).
There’s one specific thing regarding the textures though. Last.fm usually returns non power of two textures. These can be used with Minefield, but not with Chromium. This resizing is done server-side with a php script:

var texture = new GLGE.Texture(); texture.setSrc('ResizeImage.php?size=256&url=' + escape(images[i]));

The planes are grouped within GLGE.Group objects. The syntax for this is very simple:

var currentGroup = new GLGE.Group(); group.addObject(myObject);

The rest is self-explanatory:

var images = []; var apiKey = 'e351ab3a5309170fa0390aeaebd09304'; /** * Last.FM Service Calls */ var serviceCalls = [ 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=napalm%20death&api_key=' + apiKey), 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=metallica&api_key=' + apiKey), 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=ambulette&api_key=' + apiKey), 'proxy.php?mimeType=text/xml&url=' + escape('http://ws.audioscrobbler.com/2.0/?method=artist.gettopalbums&artist=dri&api_key=' + apiKey) ]; /** * Start the Last.FM Service Calls */ $(document).ready(function() { loadNextService(); }); /** * Get the artists's top albums */ function loadNextService() { if(serviceCalls.length == 0) { start3D(); return; } var serviceCall = serviceCalls.shift(); /** * Traverse the xml file and get the images */ $.ajax({ type: "GET", url: serviceCall, dataType: "xml", success: function(xml) { $(xml).find('album').each(function(){ $(this).find('image').each(function(){ var id = $(this).attr('size'); if(id == "large") images.push($(this).text()); }) }); loadNextService(); } }); } /** * Set up the 3D scene */ function start3D() { var doc = new GLGE.Document(); var planeGroups = []; var circleRadius = 5; var degreeStep = 30; var zStep = -11; var firstGroup; var lastGroup; doc.onLoad = function() { var renderer = new GLGE.Renderer(document.getElementById("canvas")); var scene = doc.getElement("mainscene"); camera = scene.getCamera(); renderer.setScene(scene); var planesPerRing = Math.ceil(360/degreeStep); var numPlanes = images.length - (images.length % planesPerRing); var currentGroup = new GLGE.Group(); firstGroup = currentGroup; planeGroups.push(currentGroup); scene.addObject(currentGroup); for(var i=0;i<=numPlanes;i++){ var plane = new GLGE.Object(); plane.setMesh(doc.getElement("Plane")); var material = new GLGE.Material(); var texture = new GLGE.Texture(); var materialLayer = new GLGE.MaterialLayer(); texture.setSrc('ResizeImage.php?size=256&url=' + escape(images[i])); materialLayer.setMapinput(GLGE.UV1); materialLayer.setMapto(GLGE.M_COLOR); materialLayer.setTexture(texture); material.addTexture(texture); material.addMaterialLayer(materialLayer); plane.setMaterial(material); var degrees = (i * degreeStep) % 360; var radians = ( Math.PI / 180 ) * degrees; var degreesTan = ( degrees + 90 ) % 360; var radiansTan = ( Math.PI / 180 ) * degreesTan; plane.setScale(1.3, 5.2, 1.3); plane.setRotX(Math.PI * .5); plane.setRotY(radiansTan); plane.setLocX(Math.cos(radians) * circleRadius); plane.setLocY(Math.sin(radians) * circleRadius); currentGroup.addObject(plane); if(i%planesPerRing==0 && i > 0 && i != numPlanes) { var newGroup = new GLGE.Group(); newGroup.setLocZ(planeGroups.length*zStep); currentGroup.nextGroup = newGroup; currentGroup = newGroup; scene.addObject(currentGroup); planeGroups.push(currentGroup); } } var locXDispl = 0; var rotDispl = 0; function render() { renderer.render(); var group = firstGroup; do { group.setLocZ(group.getLocZ() + 1.5); group = group.nextGroup; } while(group); if(firstGroup.getLocZ() > .5) { firstGroup.setLocZ(lastGroup.getLocZ() + zStep); lastGroup.nextGroup = firstGroup; var nextGroup = firstGroup.nextGroup; firstGroup.nextGroup = null; lastGroup = firstGroup; firstGroup = nextGroup; } var rotDisplSin = Math.sin(rotDispl); camera.setLocX(Math.sin(locXDispl) * 3); camera.setRotY(rotDisplSin * .3); camera.setRotZ(rotDisplSin * .5); locXDispl += .05; rotDispl += .02; } lastGroup = currentGroup; setInterval(render, 1000/60); } doc.load("scene.xml"); }

Hands-on WebGL: Basic GLGE Tutorial

Wed, 06/23/2010 - 12:13

In a previous tutorial I explained how to draw a triangle on the screen with pure low-level WebGL. A lot had to be done before anything could be drawn to the screen: define shaders, set up matrices, create shader programs, depth testing, vertex attributes, etc. Pretty interesting stuff if you’re into the technical details of WebGL but a true pain if you just want to create a nice 3D scene.

Fortunately there has been quite a lot of activity around WebGL already. The specification hasn’t been finished yet but there are already numerous libraries utilising WebGL:

GLGE

For this tutorial I’m going to use GLGE. It is very easy to set up and already has loads of features (from the GLGE website):

  • Keyframe animation
  • Perpixel lighting directional lights, spot lights and point lights
  • Normal mapping
  • Animated materials
  • Skeletal animation(WIP)
  • Collada format support
  • Parallax Mapping
  • Text rendering(probably bitmap)
  • Fog
  • Depth Shadows
  • Shader based picking

This tutorial will show you how to set up a simple scene, add a box, material and a texture.
If you don’t have a WebGL-enabled browser installed, here’s how to get one.

Setting up the scene

GLGE materials, meshes, cameras, animations, etc and the scene graph can all be specified through JavaScript or XML. The most convenient way is to do it in XML. This is an example of a very basic scene:

<?xml version="1.0" ?> <glge> <mesh id="box"> <positions>1.000000,0.999999,1.000000,1.000000,1.000000,-1.000000,-1.000000,1.000000,-1.000000,1.000000,0.999999,1.000000,-1.000000,1.000000,-1.000000,-1.000000,1.000000,1.000000,-1.000000,-1.000000,-1.000000,-1.000000,-1.000000,1.000000,-1.000000,1.000000,1.000000,-1.000000,-1.000000,-1.000000,-1.000000,1.000000,1.000000,-1.000000,1.000000,-1.000000,1.000000,-1.000000,-1.000000,0.999999,-1.000001,1.000000,-1.000000,-1.000000,-1.000000,0.999999,-1.000001,1.000000,-1.000000,-1.000000,1.000000,-1.000000,-1.000000,-1.000000,1.000000,1.000000,-1.000000,1.000000,0.999999,1.000000,1.000000,-1.000000,-1.000000,1.000000,0.999999,1.000000,0.999999,-1.000001,1.000000,1.000000,-1.000000,-1.000000,1.000000,0.999999,1.000000,-1.000000,1.000000,1.000000,0.999999,-1.000001,1.000000,-1.000000,1.000000,1.000000,-1.000000,-1.000000,1.000000,0.999999,-1.000001,1.000000,1.000000,1.000000,-1.000000,1.000000,-1.000000,-1.000000,-1.000000,-1.000000,-1.000000,1.000000,1.000000,-1.000000,-1.000000,-1.000000,-1.000000,-1.000000,1.000000,-1.000000</positions> <normals>0.000000,1.000000,0.000000,0.000000,1.000000,0.000000,0.000000,1.000000,0.000000,0.000000,1.000000,0.000000,0.000000,1.000000,0.000000,0.000000,1.000000,0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-0.000000,-1.000000,-0.000000,-0.000000,-1.000000,-0.000000,-0.000000,-1.000000,-0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,1.000000,0.000000,-0.000000,1.000000,0.000000,-0.000000,1.000000,0.000000,-0.000000,1.000000,-0.000001,0.000000,1.000000,-0.000001,0.000000,1.000000,-0.000001,0.000000,-0.000000,-0.000000,1.000000,-0.000000,-0.000000,1.000000,-0.000000,-0.000000,1.000000,0.000000,-0.000000,1.000000,0.000000,-0.000000,1.000000,0.000000,-0.000000,1.000000,0.000000,0.000000,-1.000000,0.000000,0.000000,-1.000000,0.000000,0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000,0.000000,-0.000000,-1.000000</normals> <uv1>0.333333,0.498471,0.001020,0.500000,0.000000,0.001529,0.333333,0.498471,0.000000,0.001529,0.332314,0.000000,0.000000,0.998471,0.001020,0.500000,0.333333,0.501529,0.000000,0.998471,0.333333,0.501529,0.332314,1.000000,0.665647,0.500000,0.666667,0.998471,0.333333,0.501529,0.666667,0.998471,0.334353,1.000000,0.333333,0.501529,0.998981,0.000000,1.000000,0.498471,0.666667,0.001529,1.000000,0.498471,0.667686,0.500000,0.666667,0.001529,1.000000,0.501529,0.998981,1.000000,0.667686,0.500000,0.998981,1.000000,0.666667,0.998471,0.667686,0.500000,0.334353,0.500000,0.333333,0.001529,0.665647,0.000000,0.334353,0.500000,0.665647,0.000000,0.666667,0.498471</uv1> <faces>0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35</faces> </mesh> <camera id="maincamera" loc_z="20" /> <material id="boxmaterial" color="#900" /> <scene id="mainscene" camera="#maincamera" ambient_color="#fff"> <light id="mainlight" loc_y="5" type="L_POINT" /> <object id="box" mesh="#box" rot_x="-.8" rot_y=".5" material="#boxmaterial" /> </scene> </glge>

First the mesh data is defined. In this case, the mesh data is exported from Blender using the included exporter script or the Blender to WebGL script. I don’t think there is a built in way to add primitives to the scene as of now (please tell me if I’m mistaken).

Then a camera is defined. Only one property is defined, loc_z or the z position.

A simple color material is defined (#990000, crimson red). Later on we will use a texture.

Then the scene graph is defined. It consists of a simple point light and a box primitive. Note how the mesh and materials nodes are referenced by their ids (mesh=”#box” and material=”#boxmaterial”).

HTML & JavaScript

Once this is all defined we can start coding the HTML and JavaScript. First download the GLGE library files and put them in a folder called “glge”. Then include the following script in the HTML page:

<script type="text/javascript" src="glge/glge_math.js"></script> <script type="text/javascript" src="glge/glge.js"></script>

Then add a canvas element:

<canvas id="canvas" width="400" height="400"></canvas>

Now comes the scripting part. Insert this after the canvas element (explanation in the code):

<script type="text/javascript"> // -- create a document var doc = new GLGE.Document(); // -- callback function that is called when the xml document // has finished loading doc.onLoad = function() { // -- create a renderer and pass in the canvas object var renderer = new GLGE.Renderer(document.getElementById("canvas")); // -- create a new scene. the name defined in the parameter corresponds // to the id attribute of the scene element in the xml file. var scene = doc.getElement("mainscene"); // -- pass the scene to the renderer renderer.setScene(scene); // -- the rendering loop function render() { renderer.render(); } // -- render each millisecond setInterval(render, 1); } // -- load the xml containing the scene data doc.load("scene1.xml"); </script>

See a working version here.

Using textures

The color material is nice, but looks a bit boring. We can make it more interesting by adding a texture to it. In the XML document change the material to this:

<material id="boxmaterial"> <texture id="boxtexture" src="eddie.jpg" /> <material_layer texture="#boxtexture" mapinput="UV1" mapto="M_COLOR" /> </material>

Here we’ve added a new <texture> node that point to a jpeg file. Then a material layer is defined (a material can have multiple layers). The texture attribute contains a reference to the <texture> node. The mapinput attribute references the <uv1> node that was defined in the <mesh> node.

Now we need to update the <object> sub node in the scene declaration:

<object id="box" mesh="#box" rot_x="-.8" rot_y=".5" scale_x="3" scale_y="3" scale_z="3" material="#boxmaterial" />

Here we’ve added an extra material attribute that references the id attribute (#boxmaterial) of the <material> node. Note the addition of the rotation and scale attributes as well.

See a working version here.

Animation

Adding animation is really simple as well. This can all be done in the XML document.
To illustrate this we’ll add a simple spinning animation to the cube. Add this xml node under the <glge> node:

<animation_vector id="spin" frames="300"> <animation_curve channel="RotX"> <linear_point>1,0</linear_point> <linear_point>300,-6.283</linear_point> </animation_curve> </animation_vector>

Not much to it, right? You can specify the duration by passing a number to the frames attribute. Then you add an <animation_curve> node for every property you want to animate. In this case we’re rotating the y rotation (RotY). The values in the <linear_point> nodes specify the frame number and the value (in radians, -6.283 = 2 * PI = 360 degrees).

See the result here:

That’s it for this very basic tutorial. If this has whetted your appetite and you want to see more tutorials then do let me know

For a complete set of GLGE documentation follow this link.

Cloning animated 3D objects (min3D framework for Android)

Wed, 05/26/2010 - 13:59

These two examples show how you can clone animated objects with min3D. The first example makes a shallow copy of the object. All the vertices, normals and texture coordinates are shared between all the objects. Only the first object transforms all the data for each keyframe. The rest of the objects are updated automatically.

Another option is to clone an object and clone all its vertices, uv coordinates and normals. This way the animation can be controlled indepently for each object.

This example creates shallow copies:

package rozengain.min3doom; import min3d.animation.AnimationObject3d; import min3d.core.RendererActivity; import min3d.parser.IParser; import min3d.parser.Parser; import android.os.Bundle; import android.view.Window; import android.view.WindowManager; public class Min3DoomActivity extends RendererActivity { private AnimationObject3d doomGuy; @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); super.onCreate(savedInstanceState); } @Override public void initScene() { IParser parser = Parser.createParser(Parser.Type.MD2, getResources(), "rozengain.min3doom:raw/doomguy"); parser.parse(); doomGuy = parser.getParsedAnimationObject(); doomGuy.scale().x = doomGuy.scale().y = doomGuy.scale().z = .01f; doomGuy.rotation().z = -90; doomGuy.rotation().x = -90; doomGuy.position().x = -.3f; doomGuy.position().z = -1f; scene.addChild(doomGuy); doomGuy.setFps(20); doomGuy.play(); for (int i = 0; i < 3; i++) { for (int j = 0; j < 3; j++) { // -- create a shallow copy AnimationObject3d doomGuyClone = (AnimationObject3d) doomGuy .clone(false); scene.addChild(doomGuyClone); doomGuyClone.position().x += .3 * i; doomGuyClone.position().z += 1.5 * j; if(i % 2 == 1) doomGuyClone.position().z += .5; // -- prevent this object from updating the vertices. doomGuyClone.setUpdateVertices(false); } } scene.camera().position.y = 1; scene.camera().position.z = 2; } @Override public void updateScene() { } }

See the youtube video here:

Gay dancing Doom guys

This example copies the all the vertices, normals, texture coordinates, etc so that all the objects can be animated independently:

package rozengain.min3doom2; import min3d.animation.AnimationObject3d; import min3d.core.RendererActivity; import min3d.parser.IParser; import min3d.parser.Parser; import android.os.Bundle; import android.view.Window; import android.view.WindowManager; public class Min3Doom2Activity extends RendererActivity { private AnimationObject3d doomGuy; private AnimationObject3d doomGuyClone; private int frameCounter = 0; @Override protected void onCreate(Bundle savedInstanceState) { requestWindowFeature(Window.FEATURE_NO_TITLE); getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); super.onCreate(savedInstanceState); } @Override public void initScene() { IParser parser = Parser.createParser(Parser.Type.MD2, getResources(), "rozengain.min3doom2:raw/doomguy"); parser.parse(); doomGuy = parser.getParsedAnimationObject(); doomGuy.scale().x = doomGuy.scale().y = doomGuy.scale().z = .01f; doomGuy.rotation().z = -90; doomGuy.rotation().x = -90; doomGuy.position().x = -.2f; doomGuy.position().z = -.5f; scene.addChild(doomGuy); doomGuy.setFps(20); doomGuy.play(); // -- create a deep copy doomGuyClone = (AnimationObject3d) doomGuy.clone(true); doomGuyClone.position().x = .2f; scene.addChild(doomGuyClone); scene.camera().position.z = 1; } @Override public void updateScene() { // -- start playing a bit later if(frameCounter++ == 100) doomGuyClone.play(); } }

See the youtube video here:

Loading an animated MD2 file (min3D framework for Android)

Tue, 05/25/2010 - 07:37

I just updated min3D for Android with an MD2 file importer.  MD2 is a format that was developed by ID Software and originally used for Quake II and other games. MD2 can be used for static models but the main reason for using it is animation.

Min3D is now able to import 3 different filetypes:

This is what the code looks like. Check out the min3D sample application to see it running:

package min3d.sampleProject1; import min3d.animation.AnimationObject3d; import min3d.core.RendererActivity; import min3d.parser.IParser; import min3d.parser.Parser; public class ExampleLoadMD2File extends RendererActivity { private AnimationObject3d ogre; @Override public void initScene() { IParser parser = Parser.createParser(Parser.Type.MD2, getResources(), "min3d.sampleProject1:raw/ogro"); parser.parse(); ogre = parser.getParsedAnimationObject(); ogre.scale().x = ogre.scale().y = ogre.scale().z = .07f; ogre.rotation().z = -90; ogre.rotation().x = -90; scene.addChild(ogre); ogre.setFps(70); ogre.play(); } @Override public void updateScene() { } }

Here’s the result (click the image to see the youtube video):

Please note that it looks MUCH better on a handset.

Loading a 3DS file (min3D framework for Android)

Wed, 05/19/2010 - 14:10

Another update to the min3D framework for Android. A new parser that allows you to load in 3DS files (used by the Autodesk 3ds Max 3D modeling, animation and rendering software).

In the previous tutorials (Loading 3D models with the min3D framework for Android and Loading multiple 3D objects from an Obj file (min3D for Android)) there are some basic instructions regarding resource names that apply to this bit of code as well.

Here’s the code (can be found in the Google Code repository as well):

package min3d.sampleProject1; import min3d.core.Object3dContainer; import min3d.core.RendererActivity; import min3d.parser.IParser; import min3d.parser.Parser; public class ExampleLoad3DSFile extends RendererActivity { private final float CAM_RADIUS_X = 20; private final float CAM_RADIUS_Y = 15; private final float CAM_RADIUS_Z = 30; private final float ROTATION_SPEED = 1; private Object3dContainer monster; private float degrees; @Override public void initScene() { IParser parser = Parser.createParser(Parser.Type.MAX_3DS, getResources(), "min3d.sampleProject1:raw/monster_high"); parser.parse(); monster = parser.getParsedObject(); monster.scale().x = monster.scale().y = monster.scale().z = .5f; monster.position().y = -10; scene.addChild(monster); scene.camera().target = monster.position(); } @Override public void updateScene() { float radians = degrees * ((float)Math.PI / 180); scene.camera().position.x = (float)Math.cos(radians) * CAM_RADIUS_X; scene.camera().position.y = (float)Math.sin(radians) * CAM_RADIUS_Y; scene.camera().position.z = (float)Math.sin(radians) * CAM_RADIUS_Z; degrees += ROTATION_SPEED; } }

Here’s the result (click the image to see the youtube video):

Loading multiple 3D objects from an Obj file (min3D for Android)

Tue, 05/18/2010 - 08:18

I just committed an update to min3D’s .obj file parser.  Yesterday’s tutorial showed how you can import an .obj file into min3D.

The latest commit adds support for multiple files in one .obj file. In this example, the car’s body and 4 wheels are separate objects.

This is the RendererActivity code that shows you how to get the separate objects from the parsed file:

package min3d.sampleProject1; import min3d.core.Object3d; import min3d.core.Object3dContainer; import min3d.core.RendererActivity; import min3d.parser.IParser; import min3d.parser.Parser; public class ExampleLoadObjFileMultiple extends RendererActivity { private final float MAX_ROTATION = 40; private final float MAX_CAM_X = 6f; private Object3dContainer car; private Object3d tireRR; private Object3d tireRF; private Object3d tireLR; private Object3d tireLF; private int rotationDirection; private float camDirection; @Override public void initScene() { IParser parser = Parser.createParser(Parser.Type.OBJ, getResources(), "min3d.sampleProject1:raw/camaro2_obj"); parser.parse(); car = parser.getParsedObject(); scene.addChild(car); tireRR = car.getChildByName("tire_rr"); tireRF = car.getChildByName("tire_rf"); tireLR = car.getChildByName("tire_lr"); tireLF = car.getChildByName("tire_lf"); tireLF.position().x = -.6f; tireLF.position().y = 1.11f; tireLF.position().z = .3f; tireRF.position().x = .6f; tireRF.position().y = 1.11f; tireRF.position().z = .3f; tireRR.position().x = .6f; tireRR.position().y = -1.05f; tireRR.position().z = .3f; tireLR.position().x = -.6f; tireLR.position().y = -1.05f; tireLR.position().z = .3f; car.rotation().x = -90; car.rotation().z = 180; scene.camera().position.x = MAX_CAM_X; scene.camera().position.z = 3.5f; scene.camera().position.y = 3.5f; rotationDirection = 1; camDirection = -.01f; } @Override public void updateScene() { tireRF.rotation().z += rotationDirection; tireLF.rotation().z += rotationDirection; if(Math.abs(tireRF.rotation().z) >= MAX_ROTATION) rotationDirection = -rotationDirection; scene.camera().position.x += camDirection; if(Math.abs(scene.camera().position.x) >= MAX_CAM_X) camDirection = -camDirection; } }

View the result here (click on the image to see the video):

Obj parser for min3D: multiple objects

Loading 3D models with the min3D framework for Android

Mon, 05/17/2010 - 08:02

Min3D is a new 3D framework based on OpenGL ES for Android. It’s still in its early stages, but new stuff is getting added rapidly. If you have an Android phone you can download the demo .apk file and install it on your phone.

I started contributing to the framework by writing a parser for .obj files. Almost all 3D programs export to this format, so it was an obvious choice. The first version of this parser has just been committed to Google Code and I still need to add support for a couple of things:

  • multiple objects in one file
  • groups of objects
  • bump map, specular texture map, etc

Here’s an example of how to use the new Obj parser. These are the export option from Blender:

Blender to Obj exporter options

When you export materials, you can also select “copy images” .

The Obj file should be exported to [your android project]/res/raw. Both the .obj and .mtl files have the same filename and should be renamed so there are no conflicts in the resource manager. It is best to remove the dot between the file name and the extension and replace it with an underscore. Like so:

  • camaro.obj renamed to camaro_obj
  • camaro.mtl renamed to camaro_mtl

Don’t worry about changing the file names in the file itself. The parser will take care of this. Just be sure that there are no paths prefixed to the file name.

The images should be placed in [your android project]/res/drawable. You don’t need to worry about file extensions here. Here’s what the structure looks like for the demo project:

Example project structure

Now all that’s left is adding a few lines of code. It’s as simple as creation a new Activity that is based on min3D’s RenderActivity. The parser code should be added to the initScene() method:

package min3d.sampleProject1; import min3d.core.Object3dContainer; import min3d.core.RendererActivity; import min3d.parser.IParser; import min3d.parser.Parser; /** * How to load a model from a .obj file * * @author dennis.ippel * */ public class ExampleLoadObjFile extends RendererActivity { private Object3dContainer objModel; @Override public void initScene() { IParser parser = Parser.createParser(Parser.Type.OBJ, getResources(), "min3d.sampleProject1:raw/camaro_obj"); parser.parse(); objModel = parser.getParsedObject(); objModel.scale().x = objModel.scale().y = objModel.scale().z = .7f; scene.addChild(objModel); } @Override public void updateScene() { objModel.rotation().x++; objModel.rotation().z++; } }

The Parser.createParser() method takes two arguments:

  • the parser type (only obj for now)
  • the location of the obj file resource (in “package:type/entry” format)

The rest should be self-explanatory.

This is what the result looks like for the example project:

Example min3D Obj parser output