Match and bomb
Here, we build on the previous Lighting a match tutorial to create a scene with a bomb that we can detonate with a match. The code for this program will be built on the code in Lighting a match. The match.py code is practically untouched, all we want to add to the match and matchbox now are additional nodes and routes in the X3D file, a bit of extra scene set-up in init.py and an extra interaction.py that contains only four fields to model this program of mass destruction.
The learning points of this tutorial include:
- Using ParticleSystem with PointEmitter and ExplosionEmitter
- Using TimeSensor with PositionInterpolator
- Removing children of X3DGroupingNode
- Getting current time from H3Dinterface time object
- Routing and scripting
The wick and sparks
Setting the scene
<!!-- matchbomb.x3d --> <Transform translation="0.2 0.07 -0.30" rotation="-0.1 1 0 -2.5"> <Transform translation="0.2810 -0.11 0"> <Shape> <Sphere DEF="WICK_TIP" radius="0.01" /> <Appearance> <Material diffuseColor="1 0 0" transparency="1" /> <FrictionalSurface /> </Appearance> </Shape> </Transform> <Transform scale="1.5 1 1" translation="0 0 0"> <Shape> <LineSet DEF="WICK" containerField="geometry"> <Coordinate /> </LineSet> <Appearance> <Material emissiveColor="0.8 0.8 0.6"/> <FrictionalSurface /> </Appearance> </Shape> <ParticleSystem DEF="SPARKS" createParticles="true" set_enabled="false" maxParticles="80" particleLifetime="0.4" lifetimeVariation="0.25" particleSize="0.006 0.006" colorKey="0 1" geometryType="LINE"> <Color containerField="colorRamp" color="1 1 0, 1 0 0" /> <Appearance /> <PointEmitter DEF="SPARKS_EMITTER" speed="0.04" mass="0.00005" direction="0 0 0" position="0.1610 -0.1191 0" /> </ParticleSystem> <TimeSensor DEF="TS" cycleInterval="10" enabled="true" /> <PositionInterpolator DEF="POS_INTERPOL" /> <Sound maxFront="1" maxBack="1" minFront="1" intensity="1" location="0 0 0"> <AudioClip DEF="SOUND_WICK" url="./sounds/wick.wav" /> </Sound> </Transform>
As usual, we first create a representation of the bomb. We begin with the wick. In the excerpt above there are three geometries to model the wick: WICK_TIP, WICK, and SPARKS that will travel along the wick when we light it. The wick tip is there to simulate wick lighting. When the match tip touches the wick tip later, it will set the wick ablaze. We make the wick tip transparent as it is only there to detect the match.
There is also a TimeSensor and PositionInterpolator that will be used together later to simulate the burning of the wick. We determine that it will take 10 seconds to burn the entire length of the wick, and prepare to do this by setting the cycleInterval of the TimeSensor, TS to 10. To intensify the suspense, we add a clip of the sizzling sound of a bomb. We will turn this clip on when we do the explosion.
Note that the wick is modelled with a LineSet, but the coordinates of the LineSet are not specified yet. Likewise for the value and keyValue fields of the PositionInterpolator. We will write a script in init.py to generate these values.
<!!-- matchbomb.x3d --> <PythonScript DEF="INIT" url="init.py"> <PositionInterpolator USE="POS_INTERPOL" containerField="references" /> <LineSet USE="WICK" containerField="references" /> </PythonScript>
The init.py script now needs a reference to the WICK and POS_INTERPOL.
# init.py def generateWickCoords(): coords =  x_start = 0.7854 x_end = 5.4978 x = x_start count = y = 0 while x < 5.3978: y = math.sin(x) x1 = 0.035*x y1 = 0.13*y coords.append( Vec3f( float("%0.4f"%(x1)), float("%0.4f"%(y1)), 0) ) x += 0.02 return coords def generateInterpolKey( key_val_count ): delta = 1.0/(key_val_count - 1) key =  frac = 0 while frac < 1: key.append( float("%0.4f"%(frac)) ) frac += delta return key wick_coords = generateWickCoords() wick.coord.getValue().point.setValue( wick_coords ) wick.vertexCount.setValue( [len(wick_coords)] ) wick_coords.reverse() pos_interpol.keyValue.setValue( wick_coords ) pos_interpol.key.setValue( generateInterpolKey(len(wick_coords)) )
The generateWickCoords function returns a list of Vec3f modelling the wick as a bounded sine curve. The generateInterpolKey function specifically generates a list of float values that will be assigned to the key field of POS_INTERPOL.
Having called generateWickCoords we set its result (stored in wick_coords) as the coordinates to WICK, and the vertexCount field to the number of coordinates that was generated. Then wick_coords is reversed before it is set to the keyValue field of POS_INTERPOL. It was reversed beforehand so that the sparks would later travel from the tip of the wick to the bomb and not otherwise. Finally, we set the key field of the POS_INTERPOL.
<!!-- matchbomb.x3d --> <Transform translation="0 -0.04 0"> <Shape> <Sphere DEF="BOMB" radius="0.145"/> <Appearance> <Material diffuseColor="0.25 0.25 0.3" shininess="0.8" /> <SmoothSurface /> </Appearance> </Shape> </Transform> </Transform>
After adding the bomb to the scene we can now get into action!
Lighting the wick
Although it is very natural for a wick to burn through immediately right after it is lit, here we divide this interaction into two subparts; first the lighting of the wick, where the wick is lit and sparks appear at the tip, and second the burning of the wick, when the sparks travel through the wick as the wick gets shorter.
<!!-- matchbomb.x3d --> <PythonScript DEF="INTR" url="interaction.py"> <Sphere USE="WICK_TIP" containerField="references" /> <TimeSensor USE="TS" containerField="references" /> <ParticleSystem USE="SPARKS" containerField="references" /> ... <AudioClip USE="SOUND_WICK" containerField="references" /> ... ... </PythonScript>
These interactions we will define in Python. The code above shows the relevant references passed to the script.
<!!-- matchbomb.x3d --> <ROUTE fromNode="FIRE" fromField="enabled" toNode="INTR" toField="lightWick" /> <ROUTE fromNode="WICK_TIP" fromField="isTouched" toNode="INTR" toField="lightWick" /> <ROUTE fromNode="INTR" fromField="lightWick" toNode="SPARKS" toField="set_enabled" />
We now set up the routes that will be used for the wick-lighting. The interaction is simple: we want the wick to be lit when the match tip touches the wick tip. However, we do not want the wick to light up unless the match is already lit. So we create a self-defined field lightWick that has the enabled field of the match flame and the isTouched field of the wick tip routed to it. It checks the conditions of these two events and if they are right, it will send an event to enable SPARKS.
# interaction.py class LightWick( TypedField( SFBool, (SFBool, MFBool) ) ): def update( self, event ): ri = self.getRoutesIn() match_is_lit = ri.getValue() wick_tip_touched = ri.getValue() if match_is_lit and len(wick_tip_touched) > 0 and wick_tip_touched: ri.unroute(self) ri.unroute(self) wick_tip.radius.setValue( 0 ) ts.startTime.setValue( time.getValue() ) s_wick.startTime.setValue( time.getValue() ) return True return False lightWick = LightWick()
The definition of the lightWick field is pretty straightforward. It sends a True value when the conditions are met and false otherwise. Since the wick can only be lit once, as soon as the conditions are met there is no need for the wick (and it would be erroneous) to continue receiving events from enabled field of flame and isTouched of wick tip. As such we unroute the fields. We also graphically and haptically rid the phantom wick tip by setting its radius to zero.
Now, to get our sparks ready to to travel through the wick, we set the startTime of the TimeSensor ts to the current time and enable the sound clip for the wick.
Burning the wick
<!!--matchbomb.x3d --> <ROUTE fromNode="TS" fromField="fraction_changed" toNode="POS_INTERPOL" toField="set_fraction" /> <ROUTE fromNode="POS_INTERPOL" fromField="value_changed" toNode="SPARKS_EMITTER" toField="position" />
We can model the burning of the wick very discretely with yet two more subparts: the travelling of the sparks from wick tip to wick end, and the shortening of the wick. The wick travel can be achieved with only two routes, as shown above. Previously, when we lit the wick we have also set the startTime of the TimeSensor node and activated it. It now continuously sends events to the set_fraction field of POS_INTERPOLATOR, resulting in value change. This value_changed is routed to the position of the SPARKS_EMITTER, so the sparks will travel from tip to end in the time specified in cycleInterval of the TimeSensor node.
Our task is not yet finished, as we also need to shorten the wick as it burns, naturally speaking. Programmatically speaking, we shorten the wick as the sparks travels through it:
<!!--matchbomb.x3d --> <ROUTE fromNode="SPARKS_EMITTER" fromField="position" toNode="INTR" toField="wickLength" /> <ROUTE fromNode="INTR" fromField="wickLength" toNode="INTR" toField="explode" />
To do this we route the position field of SPARKS_EMITTER to a self-defined field wickLength. by the end of the wick burning we want the bomb to go off, route wickLength to yet another self-defined field explode to signal it when the bomb is "ready" to explode. But first, the wickLength field:
# interaction.py class WickLength( AutoUpdate(TypedField(SFBool, SFVec3f)) ): def update( self, event ): sparks_pos = event.getValue() wick_points = wick.coord.getValue().point.getValue() wick_len = len( wick_points ) while wick_len > 2 and (sparks_pos.x < wick_points[wick_len-1].x): wick_points.pop() wick_len = len( wick_points ) wick.vertexCount.setValue( [wick_len] ) wick.coord.getValue().point.setValue( wick_points ) if wick_len < 3: ts.enabled.setValue(False) return True return False wickLength = WickLength()
wickLength actually does two things: first it shortens the wick as the sparks travel, and second it watches the condition that calls for an explosion i.e. when the wick has burned out. When explosion should occur it sends a True flag.
To shorten the wick it first gets the position coordinate of the sparks (sparks_pos), the a list of coordinates representing the wick (wick_points), and the number points that the wick is composed of (wick_len). Since X3D requires the minimum number of points for a LineSet to be two, it checks that condition. More importantly for our purpose here it compares the x-component of sparks_pos to the x-component of the point at the tip of the wick. If it is less, then the wick should be "burned". We "burn" or shorten the wick by removing the last point at the tip, decreasing wick_len, and resetting the vertexCount and coord of WICK to these new values. Note that we can assume x-component comparison because we model the wick from a sine curve.
When wick_len is less than three we have reached the limit of removing any more points from the WICK LineSet. So we disable the TimeSensor TS to prevent it from sending further events, and send out a True flag for the bomb to go kaboom!
<!!-- matchbomb.x3d --> <Transform translation="0.2 0.03 -0.30"> <ParticleSystem DEF="EXPLOSION" createParticles="true" enabled="false" maxParticles="2000" particleLifetime="12" lifetimeVariation="0.5" particleSize="0.3 0.3" colorKey="0.5 0.7 0.9 1" geometryType="SPRITE"> <ColorRGBA containerField="colorRamp" color="1 1 0 1, 1 0 0 1, 0 0 0 1, 1 1 1 1" /> <Appearance> <Material /> <ImageTexture url="./images/smoke.png" /> </Appearance> <TextureCoordinate point="0 0.5, 0 0.5, 0 0.5, 0 0.5, 0.5 0.5, 0.5 0.5, 0.5 0.5, 0.5 0.5, 0 0.5, 0 0.5, 0 0.5, 0 0.5" containerField="texCoordRamp" /> <ExplosionEmitter speed="0.9" mass="0.002" variation="0.5" surfaceArea="0.02" /> </ParticleSystem> </Transform> <Sound maxFront="1" maxBack="1" minFront="1" intensity="1"> <AudioClip DEF="SOUND_BOMB" url="./sounds/bomb.wav" /> </Sound>
The explosion takes the form of yet another ParticleSystem DEF-ed EXPLOSION, this time with an ExplosionEmitter. Of course do not forget the accompanying sound effect as well. Let the wick, we disable both the sound and the particle system for now, and will enable them when when the bomb is supposed to go off.
<Group DEF="SCENE"> <!!-- code for matchbox --> <!!-- code for fire --> <!!-- code for wick and sparks --> <!!-- code for bomb --> </Group>
When the explosion occur the scene whould be wiped out. Here we enclose all nodes pertaining to the scene objects in a Group node DEF-ed SCENE. We will later remove all the scene objects by clearing all the children of SCENE.
<!!-- matchbomb.x3d --> <PythonScript DEF="INTR" url="interaction.py"> ... ... ... <ParticleSystem USE="EXPLOSION" containerField="references" /> ... <AudioClip USE="SOUND_BOMB" containerField="references" /> <Group USE="SCENE" containerField="references" /> </PythonScript>
We pass in the node explosion-relevant node references to the interaction.py script.
<!!-- matchbomb.x3d --> <ROUTE fromNode="INTR" fromField="explode" toNode="EXPLOSION" toField="set_enabled" />
The model of the explosion is very simple. Earlier we have seen that the wickLength sends a boolean flag to self-defined explode field indicating when the bomb should expode. We want three things to happen when wickLength sends out True:
- enable EXPLOSION
- enable explosion sound effect
- erase scene objects
The self defined explode field does this by routing to EXPLOSION, and doing the other two tasks within itself.
# interaction.py class Explode( SFBool ): def update( self, event ): bomb_should_explode = event.getValue() if bomb_should_explode: s_bomb.startTime.setValue( time.getValue() ) scene.children.clear() return True return False explode = Explode()
If the event received is True, the explosion's update method enables the sound effect by setting its startTime to the current time, and removes all the children of the Group node SCENE by calling clear.
And there you have it, an exploding haptic bomb!