# # File:: scenexml.rb # Description:: SceneXml 3dsmax exporter XML file loader. # # Author:: David Muir # Date:: 30 October 2008 # #----------------------------------------------------------------------------- # Uses #----------------------------------------------------------------------------- require 'pipeline/fileformats/scenexml_attrs' require 'pipeline/log/log' require 'pipeline/math/bbox' require 'pipeline/math/bsphere' require 'pipeline/math/quat' require 'pipeline/math/vector3' require 'pipeline/math/vector4' require 'pipeline/math/matrix34' require 'xml' require 'uuidtools' #----------------------------------------------------------------------------- # Implementation #----------------------------------------------------------------------------- module Pipeline module FileFormats # # == Description # The SceneXml module contains all of the SceneXml related classes, # including the XML parsers and in-memory SceneXml tree classes. # # The module is expanded in the scenexml_ide.rb and scenexml_ipl.rb files # to include IDE and IPL file serialisers for map export. # module SceneXml # Loader version. VERSION = 1.20 # # == Description # SceneXml scene object. This class is an abstraction of the 3dsmax # scene information written by the SceneXml exporter, in XML format. # # The scene information is quite limited but should include enough to # be useful for validation and IDE/IPL creation. # class Scene WALK_MODE_DEPTH_FIRST = 0 WALK_MODE_BREADTH_FIRST = 1 attr_reader :filename attr_reader :version attr_reader :export_filename attr_reader :export_timestamp attr_reader :export_user attr_reader :objects attr_reader :materialdb def initialize( filename ) Scene::log().info( "Loading SceneXml from #{filename}." ) @filename = filename @lookup_table = {} parse( ) build_object_lookup_table( ) end # # Add an object reference (UUID and ObjectDef). # def add_object( obj ) @lookup_table[obj.guid] = obj end # # Find an object by UUID reference. # def find_object( uuid ) return nil unless ( @lookup_table.has_key?( uuid ) ) @lookup_table[uuid] end # # Walk down a ScemeXml::Scene object definitions invoking the user- # defined code block with the current object and its parent (nil if # its the root). # # The user should not supply a value for the parent parameter. # # === Example Usage # scene.walk( SceneXml::Scene::WALK_MODE_DEPTH_FIRST ) do |node, parent| # puts "Node: #{node.name}, class: #{node.class}" # end # def walk( mode = WALK_MODE_DEPTH_FIRST, &block ) @objects.each do |node| # Invoke parent-less node. yield( node ) if ( block_given? ) # Recurse walk_internal( mode, node, &block ) end end def Scene::log() @@log = Pipeline::Log.new( 'scenexml' ) if ( @@log.nil? ) @@log end #--------------------------------------------------------------------- # Protected #--------------------------------------------------------------------- protected @@log = nil #--------------------------------------------------------------------- # Private #--------------------------------------------------------------------- private # # Create an internal Hash to provide lookup services for ObjectDef # objects. This should make resolving ObjectDef links quicker. # def build_object_lookup_table( ) Scene::log().info( "Building SceneXml object lookup table." ) start = Time.now() walk() do |object| # Complete LODHierarchy references object.lods.resolve_guids( self ) unless ( object.lods.nil? ) end Scene::log().debug( " LOD resolution time: #{Time.now()-start}s." ) end def walk_internal( mode, node, &block ) case mode when WALK_MODE_DEPTH_FIRST # Process children, depth first by processing their children # before moving onto neighbour. node.children.each do |child_node| # Invoke child node. yield( child_node ) walk_internal( mode, child_node, &block ) end when WALK_MODE_BREADTH_FIRST # Process children, breadth first by processing all children # first before then recursing through children. node.children.each do |child_node| # Invoke child node. yield( child_node ) end node.children.each do |child_node| walk_internal( mode, child_node, &block ) end else throw RuntimeError.new( "Invalid walk mode #{mode}." ) end end def parse( ) begin start = Time.now( ) xmldoc = LibXML::XML::Document::file( @filename ) Scene::log().info( "LibXML2 XML parse took: #{Time.now()-start}s." ) raise RuntimeError.new( "Libxml2 XML parse fail." ) \ if ( xmldoc.nil? ) @version = xmldoc.root['version'].to_f if ( SceneXml::VERSION != @version ) then Scene::log().warn( "Version numbers differ, some scene information may be lost (#{@version}, #{SceneXml::VERSION})." ) end @export_filename = xmldoc.root['filename'] @export_timestamp = DateTime::parse( xmldoc.root['timestamp'] ) @export_user = xmldoc.root['user'] start = Time.now( ) @objects = parse_objects( xmldoc ) Scene::log().info( "Scene object parse took: #{Time.now()-start}s." ) start = Time.now( ) @materialdb = SceneXml::MaterialDB::from_xml( xmldoc ) Scene::log().info( "Scene material parse took: #{Time.now()-start}s." ) xmldoc = nil rescue Exception => ex Scene::log().exception( ex, "Exception parsing SceneXml #{@filename}" ) puts "Unhandled exception parsing SceneXml: #{@filename} #{ex.message}." ex.backtrace.each do |m| Scene::log().error( m ); end throw end end # # Parse all object information, returning an Array of ObjectDef objects. # def parse_objects( xmldoc ) @objects = [] xmldoc.find( '//scene/objects/object' ).each do |xml_node| object = SceneXml::ObjectDef::from_xml( self, xml_node ) add_object( object ) @objects << object end @objects end end # # == Description # SceneXml object definition. # class ObjectDef #---------------------------------------------------------------- # Attribute Accessor Methods #---------------------------------------------------------------- attr_reader :name attr_reader :guid # GUID attr_reader :parent attr_reader :classname # 3dsmax classname (friendly) attr_reader :superclass # 3dsmax superclass (friendly) attr_reader :boundingbox attr_reader :transform # TransformDef object attr_reader :attribute_class attr_reader :attributes # Hash of AttributeDef objects/ attr_reader :material # Associated material GUID attr_reader :subobjects # Array of subobject ObjectDefs. attr_reader :children # Array of children ObjectDefs. attr_reader :lods # LODHierarchyDef object attr_accessor :reffile attr_accessor :refname #---------------------------------------------------------------- # Virtual Attributes #---------------------------------------------------------------- def parent=( obj ) throw RuntimeError.new( "Cannot set parent as its already been set." ) \ unless ( @parent.nil? ) throw ArgumentError.new( "Invalid parent object, must be ObjectDef (#{obj.class})." ) \ unless ( obj.is_a?( SceneXml::ObjectDef ) ) @parent = obj end #---------------------------------------------------------------- # Instance Methods #---------------------------------------------------------------- def initialize( guid, name, _class, _superclass, bbox, transform, attrs_class, attrs, params, props, subobjects, children, lods ) #throw ArgumentError.new( "Invalid UUID (#{name}, #{guid.class})." ) \ # unless ( gui.nil? or guid.is_a?( UUID ) ) #throw ArgumentError.new( "Invalid transform (#{name}, #{transform.class})." ) \ # unless ( transform.nil? or transform.is_a?( TransformDef ) ) #throw ArgumentError.new( "Invalid bounding box object (#{name}, #{bbox.class})." ) \ # unless ( bbox.nil? or bbox.is_a?( Math::BoundingBox3 ) ) #throw ArgumentError.new( "Invalid attribute class (#{name}, #{attrs_class})." ) \ # if ( attrs_class.nil? and attrs.size > 0 ) #throw ArgumentError.new( "Invalid LOD hierarchy class (#{name}, #{lods.class})." ) \ # unless ( lods.nil? or lods.is_a?( LODHierarchyDef ) ) @name = name @guid = guid @parent = nil @classname = _class @superclass = _superclass @boundingbox = bbox @transform = transform @attribute_class = attrs_class.nil? ? '' : attrs_class.downcase @attributes = attrs @parameters = params @properties = props @subobjects = subobjects @children = children @lods = lods end # # Return the object name to be used in the IDE/IPL file. This is object # class dependent. # def get_object_name( ) if ( is_xref? or is_internalref? ) then return @refname elsif ( is_milotri? ) @name.sub( '_milo_', '' ) elsif ( is_fragment? ) then @name.sub( '_frag_', '' ) elsif ( is_2dfx? ) then if ( is_2dfx_light_effect? and not @parent.nil? ) @parent.name elsif ( is_2dfx_particle_effect? and not @parent.nil? ) @parent.name elsif ( is_2dfx_explosion_effect? and not @parent.nil? ) @parent.name elsif ( is_2dfx_swayable_effect? and not @parent.nil? ) @parent.name end return @name unless ( @parent.nil? ) return "Scene Root" if ( @parent.nil? ) else @name end end # # Return whether or not this object is a LOD child. # # An object is considered a LOD if it has a LOD parent as defined # in its LODHierarchyDef structure. # def is_lod?( ) return ( false ) unless ( @lods ) return ( false ) if ( @lods.parent_guid.nil? ) true end # # Return an ObjectDef reference (or nil) for the LOD parent of # this ObjectDef. # def get_lod_parent( ) return nil unless ( is_lod? ) @lods.parent end # # Return an Array of ObjectDef references for the LOD children of # this ObjectDef. # def get_lod_children( ) return [] if ( @lods.nil? ) return [] unless ( @lods.children.is_a?( Array ) ) return [] unless ( @lods.children.size > 0 ) @lods.children end #---------------------------------------------------------------- # Class Methods #---------------------------------------------------------------- # # Parse a REXML::Element, creating a ObjectDef instance. # def ObjectDef::from_xml( scene, xml_node ) #raise RuntimeError.new( "Invalid object node, name: #{xml_node.name}." ) \ # unless ( 'object' == xml_node.name ) guid = nil guid = UUID::parse( xml_node['guid'].gsub( /[{|}]/, '' ) ) unless ( xml_node['guid'].nil? ) name = xml_node['name'] _class = xml_node['class'] _superclass = xml_node['superclass'] refname = xml_node['refname'] reffile = xml_node['reffile'] bbox_node = xml_node.find_first( 'boundingbox3' ) bbox = Math::BoundingBox3::from_xml( bbox_node ) unless ( bbox_node.nil? ) transform_node = xml_node.find_first( 'transform' ) transform = nil transform = TransformDef::from_xml( transform_node ) unless ( transform_node.nil? ) attributes_node = xml_node.find_first( 'attributes' ) attributes = {} attributes_class = nil begin attributes_class = attributes_node['class'] xml_node.find( 'attributes/attribute' ).each do |attr_node| attr = AttributeDef::from_xml( attr_node ) attributes[attr.name] = attr end end unless ( attributes_node.nil? ) params = {} params_node = xml_node.find_first( 'paramblocks' ) begin xml_node.find( 'paramblocks/paramblock/parameter' ).each do |param_node| param = AttributeDef::from_xml( param_node ) params[param.name] = param end end unless ( params_node.nil? ) properties = {} properties_node = xml_node.find_first( 'properties' ) begin xml_node.find( 'properties/property' ).each do |property_node| property = AttributeDef::from_xml( property_node ) properties[property.name] = property end end unless ( properties_node.nil? ) children = [] children_node = xml_node.find_first( 'children' ) begin xml_node.find( 'children/object' ).each do |child_node| child = ObjectDef::from_xml( scene, child_node ) children << child scene.add_object( child ) end end unless ( children_node.nil? ) subobjects = [] subobjects_node = xml_node.find_first( 'subobjects' ) begin xml_node.find( 'subobjects/object' ).each do |subobj_node| subobjects << ObjectDef::from_xml( scene, subobj_node ) end end unless ( subobjects_node.nil? ) lods_node = xml_node.find_first( 'lod_hierarchy' ) lods = LODHierarchyDef::from_xml( lods_node ) obj = ObjectDef.new( guid, name, _class, _superclass, bbox, transform, attributes_class, attributes, params, properties, subobjects, children, lods ) obj.refname = refname obj.reffile = reffile # Fixup child and subobject parent references. obj.children.each do |child| child.parent = obj end obj.subobjects.each do |subobj| subobj.parent = obj end obj end end # # == Description # SceneXml attribute definition class, representing an attribute name, # value and type. # class AttributeDef attr_reader :name attr_reader :type attr_reader :value def initialize( name, type, value ) @name = name @type = type @value = value end # # Create a SceneXMLObjectAttrDef object from an XML node. # def AttributeDef::from_xml( xml_node ) name = xml_node['name'].downcase type = xml_node['type'].downcase value = nil case type when 'array' value = [] xml_node.find( 'parameter' ).each do |child_node| value << AttributeDef::from_xml( child_node ) end when 'int' value = xml_node['value'].to_i when 'float' value = xml_node['value'].to_f when 'bool' value = ( 'true' == xml_node['value'] ? true : false ) when 'rgb' value = Math::Vector3::from_xml( xml_node.find_first( 'colour' ) ) when 'rgba' value = Math::Vector4::from_xml( xml_node.find_first( 'colour' ) ) else # default string value = xml_node['value'] end AttributeDef.new( name, type, value ) end end # # == Description # Object transformation definition consisting of the object's # transformation matrix. # class TransformDef attr_reader :matrix # Matrix34 object #---------------------------------------------------------------- # Virtual Attributes #---------------------------------------------------------------- def position @matrix.d end def rotation Math::Quat::from_matrix34( @matrix ) end def initialize( matrix ) throw ArgumentError.new( "Invalid Matrix34 object (#{matrix.class})." ) \ unless ( matrix.is_a?( Math::Matrix34 ) ) @matrix = matrix end #---------------------------------------------------------------- # Class Methods #---------------------------------------------------------------- # # Construct a TransformDef object from an XML node. # def TransformDef::from_xml( xml_node ) #throw RuntimeError.new( "Invalid transform node, name: #{xml_node.name}." ) \ # unless ( 'transform' == xml_node.name ) # Matrix Transform Data matrix = nil mat_node = xml_node.find_first( 'matrix' ) pos_node = xml_node.find_first( 'position' ) rot_node = xml_node.find_first( 'rotation' ) if ( mat_node ) then # Have an XML matrix defined, so erm, use it. matrix = Math::Matrix34::from_xml( mat_node ) elsif ( pos_node ) # Have position, and maybe rotation. Make matrix. pos = Math::Vector3::from_xml( pos_node ) rot = Math::Quat::from_xml( rot_node ) unless ( rot_node.nil? ) if ( rot.nil? ) then matrix = Math::Matrix34.new matrix.translation = pos else matrix = Math::Matrix34::from_quat( rot ) matrix.translation = pos end end TransformDef::new( matrix ) end end # # == Description # LOD hierarchy definition for an ObjectDef. This class contains # references to LOD parent and LOD children (if applicable). # class LODHierarchyDef attr_reader :parent # ObjectDef or nil link to parent attr_reader :children # Array of ObjectDef or [] to children attr_reader :parent_guid # UUID object attr_reader :child_guids # Array of UUID objects def initialize( parent_guid, child_guids ) @parent_guid = parent_guid @child_guids = child_guids end def resolve_guids( scene ) @parent = scene.find_object( parent_guid ) @children = [] @child_guids.each do |g| @children << scene.find_object( g ) end end #---------------------------------------------------------------- # Class Methods #---------------------------------------------------------------- # # Construct a LODDef object from an XML node. # def LODHierarchyDef::from_xml( xml_node ) return nil if ( xml_node.nil? ) #throw RuntimeError.new( "Invalid LOD hierarchy node, name: #{xml_node.name}." ) \ # unless ( 'lod_hierarchy' == xml_node.name ) # Parent parent = nil parent_node = xml_node.find_first( 'parent' ) parent = UUID::parse( parent_node['guid'].gsub( /[{|}]/, '' ) ) unless ( parent_node.nil? ) # Children children = [] xml_node.find( 'children/child' ).each do |child_node| children << UUID::parse( child_node['guid'].gsub( /[{|}]/, '' ) ) end LODHierarchyDef.new( parent, children ) end end # # == Description # Individual Material definition. A material definition currently just # lists child materials and a list of textures. # # The unique UUID (:guid) can be used to link MaterialDef and # ObjectDef objects. # class MaterialDef attr_reader :name attr_reader :type attr_reader :guid attr_reader :textures attr_reader :children def initialize( name, type, guid, textures, children ) @name = name @type = type @guid = guid @textures = textures @children = children end # # Construct a MaterialDef object from an XML node. # def MaterialDef::from_xml( xml_node ) name = xml_node['name'] type = xml_node['type'] guid = UUID::parse( xml_node['guid'].gsub( /[{|}]/, '' ) ) textures = [] xml_node.find( 'textures/texture' ).each do |tex_node| filename = tex_node.attributes['filename'] textures << filename end children = {} xml_node.find( 'submaterials/material' ).each do |mat_node| submat = MaterialDef::from_xml( mat_node ) raise RuntimeError.new( "Sub-material GUID already in database, #{submat.guid}." ) \ if ( children.has_key?( submat.guid ) ) children[submat.guid] = submat end MaterialDef.new( name, type, guid, textures, children ) end end # # == Description # SceneXml material database. This class is an abstraction of all of the # materials in the 3dsmax scene, and the minimal properties that are # written to the SceneXml XML file. # class MaterialDB attr_reader :materials def initialize( materials ) @materials = materials end # # Construct a MaterialDB object from an XML node. # def MaterialDB::from_xml( xmldoc ) materials = {} xmldoc.find( '//scene/materials/material' ).each do |xml_node| mat = MaterialDef::from_xml( xml_node ) raise RuntimeError.new( "Material GUID already in database, #{mat.guid}." ) \ if ( materials.has_key?( mat.guid ) ) materials[mat.guid] = mat end MaterialDB::new( materials ) end end # # == Description # Base serialiser class that will take the SceneXml representation and # serialise it to disk file. This is the base class for both the IDE # and IPL serialisation classes and ensures they have a similar # representation. # class Serialiser attr_reader :scene def initialize( scene ) @scene = scene end def write( filename, options = {} ) throw RuntimeError.new( "Virtual method, implement in concrete serialiser class." ) end end end # SceneXml module end # FileFormats module end # Pipeline module # scenexml.rb