X3D: Create 3D Graphics with Animation

There are some great 3D graphic packages out there. Unfortunately a lot of these packages have a pretty large learning curve. If you have some simple requirements and you want to get up and running fast X3D might be a good fit.

Extensible 3D (X3D) is a standard for the creation of 3D graphics and it’s an extension to the basic HTML code. X3D is supported on all major browser and if you’re familiar with Javascript and HTML syntax the learning curve is pretty quick.

This blog documents my notes and testing. I was looking at some X3D basics such as:

  • Create simple buttons with onclick events
  • Create viewpoints
  • Create inline models (for re-use)
  • Create a two button / two pump simulation
  • Create a tank filling simulation

Getting Started

There are some great tools like Unity3D and Adobe Maya that can be used to create 3D models and worlds.

For a first example, a button will be create from scratch. An onclick event will be used to change the colour and text.

<html> 
   <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 
     <title>Button Example</title> 
     <script type='text/javascript' src='https://www.x3dom.org/download/x3dom.js'> </script> 
     <link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'></link>
     <script language="javascript">
		// Toggle Button colour and text
		function on_off() {
			if (document.getElementById('btn_col').getAttribute('diffuseColor') ==  '1 0 0') {
				document.getElementById('btn_col').setAttribute('diffuseColor', '0 1 0');
				document.getElementById('btn_text').setAttribute('string', 'On');
			} else {
				document.getElementById('btn_col').setAttribute('diffuseColor', '1 0 0');
				document.getElementById('btn_text').setAttribute('string', 'Off');			
			}
		}
	 </script>
	 
   </head> 
   <body> 
     <h1>Button Example</h1> 
 	
	 <x3d  width='100%' height='100%'> 
  
	   	<scene>
			<Viewpoint position="1.62945 0.74856 3.91868" orientation="-0.63681 0.76606 0.08730 0.50487" 
	zNear="2.56413" zFar="6.15946" centerOfRotation="0.00000 0.00000 0.00000" fieldOfView="0.78540" description="defaultX3DViewpointNode"></Viewpoint>
			<Shape>							
				<Appearance>
					<Material diffuseColor="0.8 0.8 0.8" />
				</Appearance>
	   			<Box size="1,1,1"/>
			</Shape>
			
			<Transform translation='0 0 0.4'>
				<Shape onclick="on_off();">
					<Appearance>
						<Material id="btn_col" diffuseColor="1 0 0" />
					</Appearance>

					<Sphere radius="0.4">
				</Shape>
			</Transform>
				
			<Transform translation='0 0 0.81'>					
				<Shape onclick="on_off();">
					<Appearance>
						<Material diffuseColor="0 0 0" />
					</Appearance>
					<Text id="btn_text" string="Off">
						<FontStyle size='0.25' />
					</Text>
				</Shape>
			</Transform>

		</scene> 
	</x3d> 
  </body> 
</html> 

The X3D viewer is built into all modern Web browsers. The x3d tag defines the size of the view space with a scene tag inside.

For this example there are 3 shapes, a box at the default co-ordinates, a sphere positioned in front of the box (on z-axis) and text in front of the sphere. The Transform tag is used to position items. Color is configured using a Material tag (as part of the Appearance tag).

The getAttribute and setAttribute function calls are used to adjust X3D items.

Navigation and Viewpoints

While the scene is being viewed the mouse and keyboard can be used to navigate around the scene.

Viewpoints can be created using the X3D debug mode:

  • Enter “d” – for debug mode,
  • use the mouse and keyboard to get a view that you like
  • Enter “v” – to see the viewpoint tag (copy this into your code)

It is useful to create multiple viewpoints and then have a mechanism to toggle between them. This can be done by giving the different viewpoints an ID:

<Viewpoint id="vdefault" position="1.62945 0.74856 3.91868" orientation="-0.63681 0.76606 0.08730 0.50487" 
	zNear="2.56413" zFar="6.15946" centerOfRotation="0.00000 0.00000 0.00000" fieldOfView="0.78540" description="defaultX3DViewpointNode">
</Viewpoint>

<Viewpoint id="top" position="1.07778 2.85553 3.04234" orientation="-0.89982 0.36019 0.24613 0.94283" 
	zNear="2.56413" zFar="6.15946" centerOfRotation="0.00000 0.00000 0.00000" fieldOfView="0.78540" description="defaultX3DViewpointNode">
</Viewpoint>

<Viewpoint id="bottom" position="1.89463 -2.26667 3.13753" orientation="0.68467 0.72282 0.09357 0.64533" 
	zNear="2.56413" zFar="6.15946" centerOfRotation="0.00000 0.00000 0.00000" fieldOfView="0.78540" description="defaultX3DViewpointNode">
</Viewpoint>

The next step is to have buttons, and use an onlick event to enable the “set_bind” attribute:

<button onclick="document.getElementById('vdefault').setAttribute('set_bind','true');" > Default View</button>

<button onclick="document.getElementById('top').setAttribute('set_bind','true');"> Top View</button>

<button onclick="document.getElementById('bottom').setAttribute('set_bind','true');"> Bottom View</button>  

Re-using Models with Inline Files

As the next example a pump model will be created, and then it will be called twice. The button example will also be called twice to control the pumps.

The first step is to create a basic pump.

<html> 
   <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 
     <title>Basic Pump</title> 
     <script type='text/javascript' src='https://www.x3dom.org/download/x3dom.js'> </script> 
     <link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'></link> 
   </head> 
   <body> 
	 <x3d  width='800px' height='600px'>   
	   <scene>
		<Group DEF = "hpipe" description= "Horizonal pipe">
			<transform rotation="0 0 1 1.5708" translation='-0.5 -0.35 0'> 
				<Shape description= "Outer pipe" >
					<Appearance>
						<Material id="pumpcol" diffuseColor='0.8,0.8,0.8'   transparency='0' ></Material>  
					</Appearance>
					<Cylinder  radius="0.15" height="1"></Cylinder>
				</Shape>		   
			</transform>						
		</Group>
		   		   
		<transform  rotation="1 0 0 1.5708" translation='0 0 0'> 
			<Shape description= "pump body" >
				<Appearance>
					<Material id="pumpbcol" diffuseColor='0.8,0.8,0.8'   transparency='0' ></Material>  
				</Appearance>
				<Cylinder  radius="0.5" height="0.5"></Cylinder>
			</Shape>		   
		</transform>
		<transform  rotation="1 0 0 1.5708" translation='0 0 0'> 
			<Shape description= "pump body inner piece" >
				<Appearance>
					<Material id="pumpbcol" diffuseColor='0,0,0'   transparency='0' ></Material>  
				</Appearance>
				<Cylinder  radius="0.25" height="0.51"></Cylinder>
			</Shape>		   
		</transform>

		<Transform translation="1 0.7 0"> 
			<Group USE="hpipe"/>
		</Transform>
	</scene> 
	</x3d> 
   </body> 
</html> 

This pump model has a Group tag that is used to define a horizontal pipe,<Group DEF = “hpipe” >, that is used as the in coming pipe on the pump. This grouped tag is re-used for the out going pipe, <Group USE=”hpipe”> .

An Inline X3D is not the same as the HTML version of the X3D. The inline file:

  • Starts with : <?xml version=”1.0 encoding=”UTF=8?>
  • Has no: <html> or <body> tags
  • Has an <X3D>, <Scene> with </Scene> and </X3D> tags

The X3D inline file (pump_mod.x3d) is as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE X3D PUBLIC "ISO//Web3D//DTD X3D 3.0//EN" "http://www.web3d.org/specifications/x3d-3.0.dtd">
<X3D version="3.0" profile="Immersive" xmlns:xsd="http://www.w3.org/2001/XMLSchema-instance" xsd:noNamespaceSchemaLocation="http://www.web3d.org/specifications/x3d-3.0.xsd">
	<head>
		<meta name="filename" content="Deer.x3d" />
		<meta name="generator" content="Blender 2.70 (sub 0)" />
	</head>
	<Scene>
 
<Group id="pipe1" DEF = "hpipe" description= "Horizonal pipe">
	<transform rotation="0 0 1 1.5708" translation='-0.5 -0.35 0'> 
		<Shape description= "Outer pipe" >
			<Appearance>
				<Material id="pumpcol" diffuseColor='0.8,0.8,0.8'   transparency='0' ></Material>  
			</Appearance>
			<Cylinder  radius="0.15" height="1"></Cylinder>
		</Shape>		   
	</transform>			
</Group>
<transform  rotation="1 0 0 1.5708" translation='0 0 0'> 
	<Shape description= "pump body" >
		<Appearance>
			<Material id="pumpbcol" diffuseColor='0.8,0.8,0.8'   transparency='0' ></Material>  
		</Appearance>
		<Cylinder  radius="0.5" height="0.5"></Cylinder>
	</Shape>		   
</transform>
<transform  rotation="1 0 0 1.5708" translation='0 0 0'> 
	<Shape description= "pump body inner piece" >
		<Appearance>
			<Material id="pumpbcol" diffuseColor='0,0,0'   transparency='0' ></Material>  
		</Appearance>
		<Cylinder  radius="0.25" height="0.51"></Cylinder>
	</Shape>		   
</transform>

<Transform translation="1 0.7 0"> 
	<Group id="pipe2" USE="hpipe"/>
</Transform>

  </Scene> 	 
</X3D> 

Inline files can only be used with Web Servers, they can not be called locally (with a file://).

An example of inline buttons and pumps is below:

<html>
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <title>Pump Example</title>
    <script type='text/javascript' src='https://www.x3dom.org/download/x3dom.js'> </script>
    <link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'/>
     <script language="javascript">
		function toggle(butid, pumpid) {
            // turn on/off a pump and change the button color and text
			if (document.getElementById(pumpid + '__pumpcol').getAttribute('diffuseColor') ==  '0.8,0.8,0.8') {
				document.getElementById(pumpid + '__pumpcol').setAttribute('diffuseColor', '0 1 0');
				document.getElementById(pumpid + '__pumpbcol').setAttribute('diffuseColor', '0 1 0');
                document.getElementById(butid + '__btn_col').setAttribute('diffuseColor', '0 1 0');
				document.getElementById(butid + '__btn_text').setAttribute('string', 'On');
			} else {
				document.getElementById(pumpid + '__pumpcol').setAttribute('diffuseColor', '0.8,0.8,0.8');
				document.getElementById(pumpid + '__pumpbcol').setAttribute('diffuseColor', '0.8,0.8,0.8');	
                document.getElementById(butid + '__btn_col').setAttribute('diffuseColor', '1 0 0');
				document.getElementById(butid + '__btn_text').setAttribute('string', 'Off');	
			}
		}
	 </script>
    
</head>
<body>
<h1>Button/Pump Control</h1>
<x3d runtimeEnabled="True" width='800px' height='800px'>
    <Scene>
        <Viewpoint position="0.23317 2.51382 5.21442" orientation="-0.99984 0.00995 -0.01482 0.71962" 
             zNear="2.86396" zFar="10.49846" centerOfRotation="0.00000 0.00000 0.00000" fieldOfView="0.78540" >
        </Viewpoint>
        
        <Transform translation='-1 0 0'> 
           <inline nameSpaceName="pump1"   url="pump_mod.x3d" /> 
        </Transform>    
        <Transform translation= '1 0 0'> 
           <inline nameSpaceName="pump2"   url="pump_mod.x3d" /> 
        </Transform>

        <Billboard axisOfRotation='0 0 0' >

            <Transform translation='-1 -2 0'> 
               <inline nameSpaceName="button1" onclick="toggle('button1','pump1')"  url="button.x3d" /> 
            </Transform>
            <Transform translation='1 -2 0'> 
               <inline nameSpaceName="button2" onclick="toggle('button2','pump2')"  url="button.x3d" /> 
            </Transform>

        </Billboard>
    </Scene>

</x3d>
</body>
</html>

The inline nameSpaceName is used to access id’s in the inline models. The nameSpaceName precedes all of the ids in the top level code, so an id of pumpcol in the nameSpaceName of pump1 is accessed as pump1__pumpcol.

A Billboard tag is used for positioning of the buttons. A Billboard keeps its contents facing forward even if the rest of the scene is rotated. Billboards are excellent for scene controls, gauges etc.

An inline example with the pump and button files can be accessed at: http://metcalfepete.alwaysdata.net/pump2.htm

Timers

X3D files can use the standard Javascript setTimeout() function.

As an test I created a tank filling and emptying example (http://metcalfepete.alwaysdata.net/tank.htm). The filling/emptying function (fill_tank()) is called every second and the liquid level (ilevel) and liquid height is adjusted either up or down.

The pipes and the tank can be made semi-tranparent with: transparency=’0.6′ (where 0=solid, 1=fully transparent).

The liquid in the pipes is not shown by: render=”False”

The full code for this example is:

<html> 
   <head>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/> 
     <title>Tank Example</title> 
     <script type='text/javascript' src='https://www.x3dom.org/download/x3dom.js'> </script> 
     <link rel='stylesheet' type='text/css' href='https://www.x3dom.org/download/x3dom.css'></link> 
     <script language="javascript">
		var ipipe = 0;
		var ilevel = 0;
		var ito = 0; //Timeout variable
		 
		function fill_tank() {
		 	if (ito != 0 ) { clearTimeout(ito);}
		 	document.getElementById('in_liq').setAttribute('render', 'True');
		 	document.getElementById('out_liq').setAttribute('render', 'False');
		 	document.getElementById('sliq').setAttribute('render', 'True');
		 	if (ilevel < 1.8) {
				ilevel = ilevel + 0.1;
				var ypos = -1 + (ilevel/2);
				document.getElementById("tliq").setAttribute('translation', "1 " + ypos.toString() + " 0");
				document.getElementById('liquid').setAttribute('height', ilevel.toString());
				var pfill = 100*ilevel/1.8
				document.getElementById('tank101').setAttribute('string', 'Filling: ' + parseInt(pfill) + '%');
				ito = setTimeout(fill_tank,1000);
	 
			} else {
				document.getElementById('in_liq').setAttribute('render', 'False');
		 		document.getElementById('tank101').setAttribute('string', 'Tank Full');			 
			}
		}
		function empty_tank() {
			if (ito != 0 ) { clearTimeout(ito);}
		 	document.getElementById('in_liq').setAttribute('render', 'False');
		 	document.getElementById('out_liq').setAttribute('render', 'True');
		 	document.getElementById('sliq').setAttribute('render', 'True');
		 	if (ilevel >= 0.1) {
				ilevel = ilevel - 0.1;
				var ypos = -1 + (ilevel/2);
				document.getElementById("tliq").setAttribute('translation', "1 " + ypos.toString() + " 0");
				document.getElementById('liquid').setAttribute('height', ilevel.toString());
				var pfill = 100*ilevel/1.8
				document.getElementById('tank101').setAttribute('string', 'Emptying: ' + parseInt(pfill) + '%');
				ito = setTimeout(empty_tank,1000);
	 
			} else {
				document.getElementById('out_liq').setAttribute('render', 'False');
		 		document.getElementById('tank101').setAttribute('string', 'Tank Empty');			 
			}
		}	
	 </script>
   </head> 
   <body> 
     <h1>Storage Tank </h1> 
 
	 <x3d  width='1200px' height='600px'> 
  
	   <scene>
		<Viewpoint  id="frontview" position="0.87500 -0.21796 4.11873" orientation="0.00000 -1.00000 0.00000 0.03041" 
	      zNear="1.45981" zFar="10.47667" centerOfRotation="1.00000 0.00000 0.00000" fieldOfView="0.78540">
		 </Viewpoint>

		<Viewpoint id="topview" position="2.64724 1.92990 3.25410" orientation="-0.74251 0.64386 0.18471 0.71156" 
	      zNear="1.45981" zFar="10.47667" centerOfRotation="1.00000 0.00000 0.00000" fieldOfView="0.78540" >
		</Viewpoint>
		   
		<Group id="pipe1" DEF = "hpipe" description= "Horizonal pipe">
			<transform rotation="0 0 1 1.5708" translation='-1 0.5 0'> 
				<Shape description= "Outer pipe" >
					<Appearance>
						<Material diffuseColor='0.8,0.8,0.8'   transparency='0.6' ></Material>  
					</Appearance>
					<Cylinder  radius="0.15" height="2.3"></Cylinder>
				</Shape>		   
			</transform>
		</Group>
		<transform rotation="0 0 1 1.5708" translation='-1 0.5 0'> 
			<Shape id="in_liq" description= "Liquid in pipe" render="False">
				<Appearance>
					<Material diffuseColor='1.0 0 1.0'   transparency='0' ></Material>  
				</Appearance>
				<Cylinder  radius="0.10" height="2.3"></Cylinder>
			</Shape>		   
		</transform>

		<Transform translation="4 -1.25 0"> 
			<Group id="pipe2" USE="hpipe"/>
		</Transform>
		   
		<Transform rotation="0 0 1 1.5708" translation='3 -0.75 0'> 
			<Shape id="out_liq" description= "Liquid in pipe" render="False">
				<Appearance>
					<Material diffuseColor='1.0 0 1.0'   transparency='0' ></Material>  
				</Appearance>
				<Cylinder  radius="0.10" height="2.3"></Cylinder>			
			</Shape>		   			
		</Transform>
		   
		<transform translation='1 0 0'> 
			<Shape  DEF="tank" id="tank" >
				<Appearance>
					<Material diffuseColor='0.8,0.8,0.8'   transparency='0.6' ></Material>  
				</Appearance>
				<Cylinder  radius="1" height="2"></Cylinder>
			</Shape>		   
		</transform>  
		   
		<transform id="tliq" translation='1 -0.9 0'> 
			<Shape DEF="liquid" id="sliq" render="False">
				<Appearance>
					<material diffuseColor="1.0 0 1.0" transparency='0'></material> 
				</Appearance>
				<Cylinder id="liquid" radius="0.9" height="0" render="False" ></Cylinder>
			</Shape>		   
		</transform> 	   

		   
		<Transform translation='1 0 1'>
		  <Shape>
			<!-- Text -->
			<Text id="tank101" length='0' maxExtent='2' lit="false" string='"Tank 101"'>
			   <FontStyle size='0.25' />
			</Text>
			<Appearance>
				<material diffuseColor="0 0 0" specularColor="0 0 0" transparency='0'></material> 
			</Appearance>
		  </Shape>
		</Transform>		   
		   
	</scene> 
	</x3d> 
   <button onclick="fill_tank()" >Fill Tank</button>
   <button onclick="empty_tank()">Empty Tank</button>
   <button onclick="document.getElementById('frontview').setAttribute('set_bind','true');">Front View</button>
   <button onclick="document.getElementById('topview').setAttribute('set_bind','true');">Top View</button> 

   </body> 
</html> 

Final Thoughts

I just scratched the surface on X3D, and I found that it’s a huge topic. For sure that biggest issue is building the models.

It would be interesting to connect IoT data to a X3D module using MQTT or REST calls.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s