Files
2025-09-29 00:52:08 +02:00

718 lines
21 KiB
Ruby
Executable File

#
# File:: scenexml.rb
# Description:: SceneXml 3dsmax exporter XML file loader.
#
# Author:: David Muir <david.muir@rockstarnorth.com>
# 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