Lighting a match
In this tutorial we will create a scene for the user to light a match. This tutorial makes extensive reference to the code. To avoid frustration and to follow the code snippets here clearly, you may download the complete code files for this tutorial here. Note that you need H3DAPI version 2.1 or later for this to work.
- Using ParticleSystem with ConeEmitter
- Changing graphical representation of haptics device
- Transforming vectors in different coordinate systems
- Routing and scripting
Setting the scene
<!!-- match.x3d --> <IMPORT inlineDEF="H3D_EXPORTS" exportedDEF="HDEV" AS="HDEV" /> <NavigationInfo type='"NONE"' />
We will need a reference to the haptics device in this program. The IMPORT tag imports the current active haptics device, defined as HDEV, from the H3D_EXPORTS definitions. Since we still prefer to call this reference HDEV, we import it as HDEV. We then set the type of the NavigationInfo to NONE to disable users from changing the viewpoint of the scene.
<!!-- match.x3d --> <Transform translation="-0.15 -0.04 -0.04" rotation="1 0 0 0.18"> <Transform translation="0 0 0" rotation="0 1 0 -0.5"> <Shape> <Box size="0.1 0.05 0.2" /> <Appearance > <Material diffuseColor="0.79 0.97 0.97" /> <FrictionalSurface /> </Appearance> </Shape> <Transform DEF="MATCH_STRIP_TRANS" translation="0.051 0 0" rotation="0 1 0 1.5708"> <Shape> <Rectangle2D DEF="MATCH_STRIP" size="0.19 0.04" solid="true"/> <Appearance> <Material diffuseColor="0.2 0 0.2"/> <FrictionalSurface staticFriction="0.5" dynamicFriction="0.5" stiffness="0.8"/> </Appearance> </Shape> </Transform> </Transform> </Transform>
This block of code creates and places the matchbox in our scene, and makes two DEFs, one for the transform node that encloses the red phosphorous strip of the matchbox, and one for the strip itself. We will use these DEFs when we define the match-striking interaction later.
Flame and sparks
<!!-- match.x3d --> <ParticleSystem DEF="FIRE" createParticles="true" enabled="false" maxParticles="100" particleLifetime="0.8" lifetimeVariation="0.5" particleSize="0.003 0.003" colorKey="0 0.7 1" geometryType="SPRITE"> <Color containerField="colorRamp" color="1 0 0, 1 1 0, 0.3 0.3 0.3" /> <Appearance /> <ConeEmitter DEF="FIRE_EMITTER" speed="0.02" mass="0.00005" direction="0 1 0" angle="1" /> </ParticleSystem>
As the match strikes the box, there will eventually be fire. The first ParticleSystem node, DEF-ed FIRE, creates that match flame and puts it on the scene. The ParticleSystem has several fields that lets us apecify its properties. For this flame we confine the maximum particles that are emitted at any one time to 100 (maxParticles="100") and the lifetime of each particle to 0.8 seconds with 0.5 variation. We also set createParticles to "true" as we want new particles to be created continuously to simulate a flame.
The ParticleSystem has a colorRamp field of type SFColorNode that specifies the colours of the particles throughout its lifetime. In the case of this flame, we want each particle to be created yellow, turn red and then turn gray as it changes into smoke. To specify the time at which the colour changes, we insert SFloat values representing the fraction of its lifetime into the colorKey field of the ParticleSystem. Each fraction in colorKey corresponds to a colour in the colorRamp respectively i.e. in this case, the particle is yellow at the beginning, red at seven tenth of its life, and gray at the end.
The ParticleSystem node contains an emitter field of type SFEmitterNode. As its name suggests, the X3DParticleEmitterNode emits the particles of the system. The ConeEmitter is used here to create fire effects. In definition, the ConeEmitter emits particles from a specified point in a specified direction. It creates a particle distribution that arises from the particles being randomly distributed within the cone of a specified angle.
If you are like me, you will probably not light a match with the first strike. Your efforts may result in sparks. To capture the interaction as realistically as possible let us also add a ParticleSystem to model the sparks:
<!!-- match.x3d --> <ParticleSystem DEF="MATCH_SPARKS" createParticles="true" enabled="false" maxParticles="80" particleLifetime="0.4" lifetimeVariation="0.25" particleSize="0.003 0.003" colorKey="0 1" geometryType="LINE"> <Color containerField="colorRamp" color="1 1 0, 1 0 0" /> <Appearance /> <PointEmitter DEF="MATCH_SPARKS_EMITTER" speed="0.04" mass="0.00005" direction="0 0 0" /> </ParticleSystem> <PointLight DEF="FIRE_LIGHT" ambientIntensity="0.9" color="1 1 0" global="true" intensity="0.7" on="false" attenuation="1 1 1" /> <Sound maxFront="1" maxBack="1" minFront="1" intensity="1" location="0 0 0"> <AudioClip DEF="SOUND_MATCH" url="./sounds/match.wav" /> </Sound>
The significant difference between this ParticleSystem and that of the former is the use the PointEmitter, the LINE geometryType, and the shorter lifetime to model flickering sparks. Note that although we have now drawn a flame and sparks in our scene, we do not want them to be visible until we have struck the match. To ensure this we set the enabled field for both ParticleSystems to false for now.
In real life the flame from a lit match will emit light. We manually model this in our scene with a PointLight node, turned off now by setting on to false. It will be nice to simulate the flick of the match with a successful strike, and so to our scene we add the Sound node with an AudioClip. The default value of the enabled field for AudioClip is false. Thus, like the flame, sparks and light, it is disable for now.
<!!-- match.x3d --> <ROUTE fromNode="HDEV" fromField="proxyPosition" toNode="FIRE_EMITTER" toField="position" /> <ROUTE fromNode="HDEV" fromField="proxyPosition" toNode="FIRE_LIGHT" toField="location" /> <ROUTE fromNode="HDEV" fromField="proxyPosition" toNode="MATCH_SPARKS_EMITTER" toField="position" />
Finally, we route the proxyPosition of our haptics device to the position and location fields of our emitters and light respectively, as we would want the flame, sparks and light to originate from the tip of our match.
We have one thing left to do before we go on to defining the interaction of the match and matchbox: change the graphical representation of our haptics device to a match. We do this in the python script init.py.
# init.py from H3DInterface import * match, node = createX3DNodeFromString( "<Group> \ <Shape> \ <Appearance><Material DEF=\"MATCH_TIP\" diffuseColor=\"0.7 0 0.25\" /></Appearance> \ <Sphere radius=\"0.0032\"/> \ </Shape> \ <Transform translation=\"0 0 0.052\" > \ <Shape> \ <Appearance> \ <Material/><ImageTexture url=\"./images/wood.jpg\" /> \ </Appearance> \ <Box size=\"0.0048 0.0048 0.1\" /> \ </Shape> \ </Transform></Group>" )
The createX3DNodeFromString function takes a string of X3D/XML code, creates the nodes and returns a tuple where the first element is the outermost node specified in the input string and the second element a dictionay of every DEF-ed node in the string, with the DEF-ed node as the value and the corresponding DEF value as the key. Here we create the X3D model of a match.
# init.py if getActiveDeviceInfo(): devices = getActiveDeviceInfo().device.getValue() for d in devices: d.stylus.setValue( match )
We then get check if the DeviceInfo is valid. If it is then to all the devices present in the DeviceInfo (i.e. for all the active haptics devices that we have) we assign its stylus field the match that we created. Of course we must also add this script to our scenegraph:
<!!-- match.x3d --> <PythonScript DEF="INIT" url="init.py" />
Note that when we call createX3DNodeFromString we also saved a dictionary containing the Material node DEF-ed MATCH_TIP. We will use this reference when we code the match striking interaction - and that is coming up right after this!
Striking and lighting the match
Before proceeding to code the match striking interaction it may be helpful to think of the possible scenarios that may arise. If we just graze the match lightly on the red phosphorous strip we are unlikely to create any sparks, let alone fire. If we do not generate enough heat from the grazing, due to insufficient frictional force, there may be sparks but no fire. We may get fire if we strike a little faster.
From these considerations it is apparent that the successful lighting of the match will depend on two fields:
- the force field of MATCH_STRIP, which is the force the geometry applied to the haptics device a.k.a match
- the trackerVelocity of our active haptics device, which is the velocity of our haptics tracker in the world coordinate system
We may also want to only consider the components of trackerVelocity that are parallel to the plane of the MATCH_STRIP (it is unlikely that we will get any fire from poking hard and fast at the matchbox!)
To model these interactions, our strategy is to create a field in Python that receives an event from the force field of MATCH_STRIP to indicate that contact has been made by the haptics device on the MATCH_STRIP. Our field will then consider the cases above and then send an event (of SFBool type) to the enable or disable the sparks. If the conditions are right, we do not want to only enable the sparks, we also want to turn on the flame, light and the sound of match striking. We call this field of ours grazeMatch.
<!!-- match.x3d --> <PythonScript DEF="MATCH" url="match.py"> <Transform USE="MATCH_STRIP_TRANS" containerField="references" /> <ParticleSystem USE="FIRE" containerField="references" /> <PointLight USE="FIRE_LIGHT" containerField="references" /> <AudioClip USE="SOUND_MATCH" containerField="references" /> </PythonScript> <ROUTE fromNode="MATCH_STRIP" fromField="force" toNode="MATCH" toField="grazeMatch" /> <ROUTE fromNode="MATCH" fromField="grazeMatch" toNode="MATCH_SPARKS" toField="set_enabled" />
The exerpt above puts our strategy in code. We define the grazeMatch field in match.py, and route the force field of MATCH_STRIP to grazeMatch, and grazeMatch to set_enabled of MATCH_SPARKS. Since we will need to refer to the flame, light and sound clip we pass them as references to the Python script. We also pass in the references of the Transform node enclosing the MATCH_STRIP, MATCH_STRIP_TRANS. MATCH_STRIP_TRANS will be vital extracting the trackerVelocity components, as you will soon see.
# match.py from H3DInterface import * import math import INIT di = getActiveDeviceInfo() if ( di ): hdev = di.device.getValue() trans, fire, light, sound_match = references.getValue() matrix = trans.accumulatedForward.getValue() matrix = matrix.getScaleRotationPart()
In addition to the H3DInterface we will also need the Python Math library. We import INIT, which is the PythonScript defined in init.py for one sole reason: to get a reference to MATCH_TIP, of which colour we will change when the match is successfully lit. We also store all the other references that we need in respective variables.
Remember MATCH_STRIP_TRANS that we said will vital in extracting trackerVelocity component? What we need from that node is its accumulatedForward field that gives the accumulated forward matrix up to that Transform node. We apply getScaleRotationPart on the matrix and store the result in the variable matrix.
# match.py class GrazeMatch( TypedField(SFBool, MFVec3f) ): def update( self, event ): velocity = matrix*hdev.trackerVelocity.getValue() if len(event.getValue()) > 0: force_on_match = event.getValue() velocity_x = math.fabs(velocity.x)
Our field class inherits from the TypedField, and takes an input of type MFvec3f and outputs an SFBool. In the field's update method, before doing anything else we change the coordinates of the haptics device's trackerVelocity from world coordinates to coordinates local to the match strip by multiplying it with matrix that we have calculated earlier.
The input event is an MFVec3f that carries the force by MATCH_STRIP on all active haptics devices connected to the system. We check the length of this input. A length more than zero indicates presence of at least one haptics. We assume here that we are working with only the first haptics device, and retrieve the force on this device into force_on_match. We also obtain the x-component of the trackerVelocity and name it velocity_x.
# match.py if force_on_match.length() > 4 and velocity_x > 0.8: sound_match.startTime.setValue( time.getValue() ) fire.set_enabled.setValue(True) light.on.setValue(True) event.unroute(self) INIT.node["MATCH_TIP"].diffuseColor.setValue( RGB(0.1, 0.1, 0.1) ) return False if force_on_match.length() > 1 and velocity_x > 0.3: return True return False return False grazeMatch = GrazeMatch()
If the magnitude of force_on_match is more than four Newtons and the velocity of our device is more than 0.8, then we have fire. As discussed earlier, we will also enable the sound of match-striking (by seting startTime of the node to current time), and also enable the flame and light. Once we have struck the match, we no longer want to keep the interaction between the match and matchbox, so we unroute the incoming field, which is the force field of match strip, from our field. We also want to make the match tip seem burnt by setting the diffuseColor of of its Material node to dark gray. We have a reference to the match tip in the INIT script that we import. Since we already have fire, we return False to disable the sparks.
It could be that neither conditions to generate fire is met, but the force and velocity are still great enough to create sparks. We define these thresholds as one and 0.3 respectively, and return True to enable sparks.
If the either force or velocity are less than these (e.g. user is only touching the match box or lightly brushes it), our field will return False so that sparks do not appear.
Finally we create an object of this field and name it grazeField.
And that was our program! In the next tutorial we will add a bomb to the scene that we can light with our match.