"""
:mod:`pyffi.formats.nif` --- NetImmerse/Gamebryo (.nif and .kf)
===============================================================
Implementation
--------------
.. autoclass:: NifFormat
:show-inheritance:
:members:
Regression tests
----------------
These tests are used to check for functionality and bugs in the library.
They also provide code examples which you may find useful.
Read a NIF file
^^^^^^^^^^^^^^^
>>> stream = open('tests/nif/test.nif', 'rb')
>>> data = NifFormat.Data()
>>> # inspect is optional; it will not read the actual blocks
>>> data.inspect(stream)
>>> hex(data.version)
'0x14010003'
>>> data.user_version
0
>>> for blocktype in data.header.block_types:
... print(blocktype.decode("ascii"))
NiNode
NiTriShape
NiTriShapeData
>>> data.roots # blocks have not been read yet, so this is an empty list
[]
>>> data.read(stream)
>>> for root in data.roots:
... for block in root.tree():
... if isinstance(block, NifFormat.NiNode):
... print(block.name.decode("ascii"))
test
>>> stream.close()
Parse all NIF files in a directory tree
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>>> for stream, data in NifFormat.walkData('tests/nif'):
... try:
... # the replace call makes the doctest also pass on windows
... print("reading %s" % stream.name.replace("\\\\", "/"))
... data.read(stream)
... except Exception:
... print("Warning: read failed due corrupt file, corrupt format description, or bug.")
reading tests/nif/invalid.nif
Warning: read failed due corrupt file, corrupt format description, or bug.
reading tests/nif/nds.nif
reading tests/nif/neosteam.nif
reading tests/nif/test.nif
reading tests/nif/test_centerradius.nif
reading tests/nif/test_check_tangentspace1.nif
reading tests/nif/test_check_tangentspace2.nif
reading tests/nif/test_check_tangentspace3.nif
reading tests/nif/test_check_tangentspace4.nif
reading tests/nif/test_convexverticesshape.nif
reading tests/nif/test_dump_tex.nif
reading tests/nif/test_fix_clampmaterialalpha.nif
reading tests/nif/test_fix_cleanstringpalette.nif
reading tests/nif/test_fix_detachhavoktristripsdata.nif
reading tests/nif/test_fix_disableparallax.nif
reading tests/nif/test_fix_ffvt3rskinpartition.nif
reading tests/nif/test_fix_mergeskeletonroots.nif
reading tests/nif/test_fix_tangentspace.nif
reading tests/nif/test_fix_texturepath.nif
reading tests/nif/test_mopp.nif
reading tests/nif/test_opt_delunusedbones.nif
reading tests/nif/test_opt_dupgeomdata.nif
reading tests/nif/test_opt_dupverts.nif
reading tests/nif/test_opt_emptyproperties.nif
reading tests/nif/test_opt_mergeduplicates.nif
reading tests/nif/test_skincenterradius.nif
reading tests/nif/test_vertexcolor.nif
Create a NIF model from scratch and write to file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>>> root = NifFormat.NiNode()
>>> root.name = 'Scene Root'
>>> blk = NifFormat.NiNode()
>>> root.add_child(blk)
>>> blk.name = 'new block'
>>> blk.scale = 2.4
>>> blk.translation.x = 3.9
>>> blk.rotation.m_11 = 1.0
>>> blk.rotation.m_22 = 1.0
>>> blk.rotation.m_33 = 1.0
>>> ctrl = NifFormat.NiVisController()
>>> ctrl.flags = 0x000c
>>> ctrl.target = blk
>>> blk.add_controller(ctrl)
>>> blk.add_controller(NifFormat.NiAlphaController())
>>> strips = NifFormat.NiTriStrips()
>>> root.add_child(strips, front = True)
>>> strips.name = "hello world"
>>> strips.rotation.m_11 = 1.0
>>> strips.rotation.m_22 = 1.0
>>> strips.rotation.m_33 = 1.0
>>> data = NifFormat.NiTriStripsData()
>>> strips.data = data
>>> data.num_vertices = 5
>>> data.has_vertices = True
>>> data.vertices.update_size()
>>> for i, v in enumerate(data.vertices):
... v.x = 1.0+i/10.0
... v.y = 0.2+1.0/(i+1)
... v.z = 0.03
>>> data.update_center_radius()
>>> data.num_strips = 2
>>> data.strip_lengths.update_size()
>>> data.strip_lengths[0] = 3
>>> data.strip_lengths[1] = 4
>>> data.has_points = True
>>> data.points.update_size()
>>> data.points[0][0] = 0
>>> data.points[0][1] = 1
>>> data.points[0][2] = 2
>>> data.points[1][0] = 1
>>> data.points[1][1] = 2
>>> data.points[1][2] = 3
>>> data.points[1][3] = 4
>>> data.num_uv_sets = 1
>>> data.has_uv = True
>>> data.uv_sets.update_size()
>>> for i, v in enumerate(data.uv_sets[0]):
... v.u = 1.0-i/10.0
... v.v = 1.0/(i+1)
>>> data.has_normals = True
>>> data.normals.update_size()
>>> for i, v in enumerate(data.normals):
... v.x = 0.0
... v.y = 0.0
... v.z = 1.0
>>> strips.update_tangent_space()
>>> from tempfile import TemporaryFile
>>> stream = TemporaryFile()
>>> nifdata = NifFormat.Data(version=0x14010003, user_version=10)
>>> nifdata.roots = [root]
>>> nifdata.write(stream)
>>> stream.close()
Get list of versions and games
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>>> for vnum in sorted(NifFormat.versions.values()):
... print('0x%08X' % vnum) # doctest: +REPORT_UDIFF
0x02030000
0x03000000
0x03000300
0x03010000
0x0303000D
0x04000000
0x04000002
0x0401000C
0x04020002
0x04020100
0x04020200
0x0A000100
0x0A000102
0x0A000103
0x0A010000
0x0A010065
0x0A01006A
0x0A020000
0x0A020001
0x0A040001
0x14000004
0x14000005
0x14010003
0x14020007
0x14020008
0x14030001
0x14030002
0x14030003
0x14030006
0x14030009
0x14050000
0x14060000
0x1E000002
>>> for game, versions in sorted(NifFormat.games.items(), key=lambda x: x[0]):
... print("%s " % game + " ".join('0x%08X' % vnum for vnum in versions)) # doctest: +REPORT_UDIFF
? 0x0A000103
Atlantica 0x14020008
Axis and Allies 0x0A010000
Civilization IV 0x04020002 0x04020100 0x04020200 0x0A000100 0x0A010000 \
0x0A020000 0x14000004
Culpa Innata 0x04020200
Dark Age of Camelot 0x02030000 0x03000300 0x03010000 0x0401000C 0x04020100 \
0x04020200 0x0A010000
Divinity 2 0x14030009
Emerge 0x14020007 0x14020008 0x14030001 0x14030002 0x14030003 0x14030006 \
0x1E000002
Empire Earth II 0x04020200 0x0A010000
Empire Earth III 0x14020007 0x14020008
Entropia Universe 0x0A010000
Fallout 3 0x14020007
Freedom Force 0x04000000 0x04000002
Freedom Force vs. the 3rd Reich 0x0A010000
Howling Sword 0x14030009
Kohan 2 0x0A010000
KrazyRain 0x14050000 0x14060000
Lazeska 0x14030009
Loki 0x0A020000
Megami Tensei: Imagine 0x14010003
Morrowind 0x04000002
NeoSteam 0x0A010000
Oblivion 0x0303000D 0x0A000102 0x0A010065 0x0A01006A 0x0A020000 0x14000004 \
0x14000005
Prison Tycoon 0x0A020000
Pro Cycling Manager 0x0A020000
Red Ocean 0x0A020000
Sid Meier's Railroads 0x14000004
Star Trek: Bridge Commander 0x03000000 0x03010000
The Guild 2 0x0A010000
Warhammer 0x14030009
Wildlife Park 2 0x0A010000 0x0A020000
Worldshift 0x0A020001 0x0A040001
Zoo Tycoon 2 0x0A000100
Reading an unsupported nif file
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>>> stream = open('tests/nif/invalid.nif', 'rb')
>>> data = NifFormat.Data()
>>> data.inspect(stream) # the file seems ok on inspection
>>> data.read(stream) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: ...
>>> stream.close()
Template types
^^^^^^^^^^^^^^
>>> block = NifFormat.NiTextKeyExtraData()
>>> block.num_text_keys = 1
>>> block.text_keys.update_size()
>>> block.text_keys[0].time = 1.0
>>> block.text_keys[0].value = 'hi'
Links
^^^^^
>>> NifFormat.NiNode._has_links
True
>>> NifFormat.NiBone._has_links
True
>>> skelroot = NifFormat.NiNode()
>>> geom = NifFormat.NiTriShape()
>>> geom.skin_instance = NifFormat.NiSkinInstance()
>>> geom.skin_instance.skeleton_root = skelroot
>>> [block.__class__.__name__ for block in geom.get_refs()]
['NiSkinInstance']
>>> [block.__class__.__name__ for block in geom.get_links()]
['NiSkinInstance']
>>> [block.__class__.__name__ for block in geom.skin_instance.get_refs()]
[]
>>> [block.__class__.__name__ for block in geom.skin_instance.get_links()]
['NiNode']
Strings
^^^^^^^
>>> extra = NifFormat.NiTextKeyExtraData()
>>> extra.num_text_keys = 2
>>> extra.text_keys.update_size()
>>> extra.text_keys[0].time = 0.0
>>> extra.text_keys[0].value = "start"
>>> extra.text_keys[1].time = 2.0
>>> extra.text_keys[1].value = "end"
>>> for extrastr in extra.get_strings():
... print(extrastr.decode("ascii"))
start
end
"""
# ***** BEGIN LICENSE BLOCK *****
#
# Copyright (c) 2007-2009, NIF File Format Library and Tools.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials provided
# with the distribution.
#
# * Neither the name of the NIF File Format Library and Tools
# project nor the names of its contributors may be used to endorse
# or promote products derived from this software without specific
# prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# ***** END LICENSE BLOCK *****
from itertools import izip,repeat
import logging
import math # math.pi
import os
import re
import struct
import sys
import warnings
import weakref
import pyffi.formats.bsa
import pyffi.formats.dds
import pyffi.object_models.common
import pyffi.object_models
from pyffi.object_models.xml import FileFormat
import pyffi.utils.inertia
from pyffi.utils.mathutils import *# XXX todo get rid of from XXX import *
pyffi.utils.mopp
import pyffi.utils.tristrip
import pyffi.utils.quickhull
# XXX convert the following to absolute imports
from pyffi.object_models.editable import EditableBoolComboBox
from pyffi.utils.graph import EdgeFilter
from pyffi.object_models.xml.basic import BasicBase
from pyffi.object_models.xml.struct_ import StructBase
class NifFormat(FileFormat):
"""This class contains the generated classes from the xml."""
xml_file_name = 'nif.xml'
# where to look for nif.xml and in what order: NIFXMLPATH env var,
# or NifFormat module directory
xml_file_path = [os.getenv('NIFXMLPATH'),
os.path.join(os.path.dirname(__file__), "nifxml")]
# filter for recognizing nif files by extension
# .kf are nif files containing keyframes
# .kfa are nif files containing keyframes in DAoC style
# .nifcache are Empire Earth II nif files
# .texcache are Empire Earth II/III packed texture nif files
# .pcpatch are Empire Earth II/III packed texture nif files
RE_FILENAME = re.compile(r'^.*\.(nif|kf|kfa|nifcache|jmi|texcache|pcpatch)$', re.IGNORECASE)
# archives
ARCHIVE_CLASSES = [pyffi.formats.bsa.BsaFormat]
# used for comparing floats
EPSILON = 0.0001
# basic types
int = pyffi.object_models.common.Int
uint = pyffi.object_models.common.UInt
byte = pyffi.object_models.common.UByte # not a typo
char = pyffi.object_models.common.Char
short = pyffi.object_models.common.Short
ushort = pyffi.object_models.common.UShort
float = pyffi.object_models.common.Float
BlockTypeIndex = pyffi.object_models.common.UShort
StringIndex = pyffi.object_models.common.UInt
SizedString = pyffi.object_models.common.SizedString
# implementation of nif-specific basic types
class StringOffset(pyffi.object_models.common.Int):
"""This is just an integer with -1 as default value."""
def __init__(self, **kwargs):
pyffi.object_models.common.Int.__init__(self, **kwargs)
self.set_value(-1)
class bool(BasicBase, EditableBoolComboBox):
"""Basic implementation of a 32-bit (8-bit for versions > 4.0.0.2)
boolean type.
>>> i = NifFormat.bool()
>>> i.set_value('false')
>>> i.get_value()
False
>>> i.set_value('true')
>>> i.get_value()
True
"""
def __init__(self, **kwargs):
BasicBase.__init__(self, **kwargs)
self.set_value(False)
def get_value(self):
return self._value
def set_value(self, value):
if isinstance(value, basestring):
if value.lower() == 'false':
self._value = False
return
elif value == '0':
self._value = False
return
if value:
self._value = True
else:
self._value = False
def get_size(self, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
if ver > 0x04000002:
return 1
else:
return 4
def get_hash(self, **kwargs):
return self._value
def read(self, stream, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
if ver > 0x04000002:
value, = struct.unpack('<B', stream.read(1))
else:
value, = struct.unpack('<I', stream.read(4))
self._value = bool(value)
def write(self, stream, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
if ver > 0x04000002:
stream.write(struct.pack('<B', int(self._value)))
else:
stream.write(struct.pack('<I', int(self._value)))
class Flags(pyffi.object_models.common.UShort):
def __str__(self):
return hex(self.get_value())
class Ref(BasicBase):
"""Reference to another block."""
_is_template = True
_has_links = True
_has_refs = True
def __init__(self, **kwargs):
BasicBase.__init__(self, **kwargs)
self._template = kwargs.get("template")
self.set_value(None)
def get_value(self):
return self._value
def set_value(self, value):
if value is None:
self._value = None
else:
if not isinstance(value, self._template):
raise TypeError(
'expected an instance of %s but got instance of %s'
% (self._template, value.__class__))
self._value = value
def get_size(self, **kwargs):
return 4
def get_hash(self, **kwargs):
if self.get_value():
return self.get_value().get_hash(**kwargs)
else:
return None
def read(self, stream, **kwargs):
self.set_value(None) # fix_links will set this field
block_index, = struct.unpack('<i', stream.read(4))
kwargs.get('link_stack', []).append(block_index)
def write(self, stream, **kwargs):
"""Write block reference.
:keyword block_index_dct: The dictionary of block indices
(block -> index).
"""
if self.get_value() is None:
# nothing to point to
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
if ver >= 0x0303000D:
stream.write(struct.pack("<i", -1)) # link by number
else:
stream.write(struct.pack("<i", 0)) # link by pointer
else:
stream.write(struct.pack(
'<i', kwargs.get('block_index_dct')[self.get_value()]))
def fix_links(self, **kwargs):
"""Fix block links.
:keyword link_stack: The link stack.
:keyword block_dct: The block dictionary (index -> block).
"""
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
block_index = kwargs.get('link_stack').pop(0)
# case when there's no link
if ver >= 0x0303000D:
if block_index == -1: # link by block number
self.set_value(None)
return
else:
if block_index == 0: # link by pointer
self.set_value(None)
return
# other case: look up the link and check the link type
block = kwargs.get('block_dct')[block_index]
if isinstance(block, self._template):
self.set_value(block)
else:
#raise TypeError('expected an instance of %s but got instance of %s'%(self._template, block.__class__))
logging.getLogger("pyffi.nif.ref").warn(
"Expected an %s but got %s: ignoring reference."
% (self._template, block.__class__))
def get_links(self, **kwargs):
val = self.get_value()
if val is not None:
return [val]
else:
return []
def get_refs(self, **kwargs):
val = self.get_value()
if val is not None:
return [val]
else:
return []
def replace_global_node(self, oldbranch, newbranch,
edge_filter=EdgeFilter()):
"""
>>> from pyffi.formats.nif import NifFormat
>>> x = NifFormat.NiNode()
>>> y = NifFormat.NiNode()
>>> z = NifFormat.NiNode()
>>> x.add_child(y)
>>> x.children[0] is y
True
>>> x.children[0] is z
False
>>> x.replace_global_node(y, z)
>>> x.children[0] is y
False
>>> x.children[0] is z
True
>>> x.replace_global_node(z, None)
>>> x.children[0] is None
True
"""
if self.get_value() is oldbranch:
# set_value takes care of template type
self.set_value(newbranch)
#print("replacing", repr(oldbranch), "->", repr(newbranch))
if self.get_value() is not None:
self.get_value().replace_global_node(oldbranch, newbranch)
def get_detail_display(self):
# return the node itself, if it is not None
if self.get_value() is not None:
return self.get_value()
else:
return "None"
class Ptr(Ref):
"""A weak reference to another block, used to point up the hierarchy tree. The reference is not returned by the L{get_refs} function to avoid infinite recursion."""
_is_template = True
_has_links = True
_has_refs = False
# use weak reference to aid garbage collection
def get_value(self):
return self._value() if self._value is not None else None
def set_value(self, value):
if value is None:
self._value = None
else:
if not isinstance(value, self._template):
raise TypeError(
'expected an instance of %s but got instance of %s'
% (self._template, value.__class__))
self._value = weakref.ref(value)
def __str__(self):
# avoid infinite recursion
return '%s instance at 0x%08X'%(self._value.__class__, id(self._value))
def get_refs(self, **kwargs):
return []
def get_hash(self, **kwargs):
return None
def replace_global_node(self, oldbranch, newbranch,
edge_filter=EdgeFilter()):
# overridden to avoid infinite recursion
if self.get_value() is oldbranch:
self.set_value(newbranch)
#print("replacing", repr(oldbranch), "->", repr(newbranch))
class LineString(BasicBase):
"""Basic type for strings ending in a newline character (0x0a).
>>> from tempfile import TemporaryFile
>>> f = TemporaryFile()
>>> l = NifFormat.LineString()
>>> f.write('abcdefg\\x0a'.encode())
>>> f.seek(0)
>>> l.read(f)
>>> str(l)
'abcdefg'
>>> f.seek(0)
>>> l.set_value('Hi There')
>>> l.write(f)
>>> f.seek(0)
>>> m = NifFormat.LineString()
>>> m.read(f)
>>> str(m)
'Hi There'
"""
def __init__(self, **kwargs):
BasicBase.__init__(self, **kwargs)
self.set_value('')
def get_value(self):
return self._value
def set_value(self, value):
self._value = pyffi.object_models.common._as_bytes(value).rstrip('\x0a'.encode("ascii"))
def __str__(self):
return pyffi.object_models.common._as_str(self._value)
def get_size(self, **kwargs):
return len(self._value) + 1 # +1 for trailing endline
def get_hash(self, **kwargs):
return self.get_value()
def read(self, stream, **kwargs):
self._value = stream.readline().rstrip('\x0a'.encode("ascii"))
def write(self, stream, **kwargs):
stream.write(self._value)
stream.write("\x0a".encode("ascii"))
class HeaderString(BasicBase):
def __str__(self):
return 'NetImmerse/Gamebryo File Format, Version x.x.x.x'
def get_detail_display(self):
return self.__str__()
def get_hash(self, **kwargs):
return None
def read(self, stream, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
try:
modification = kwargs['data'].modification
except KeyError:
modification = None
version_string = self.version_string(ver, modification)
s = stream.read(len(version_string) + 1)
if s != (version_string + '\x0a').encode("ascii"):
raise ValueError(
"invalid NIF header: expected '%s' but got '%s'"
% (version_string, s[:-1]))
def write(self, stream, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
try:
modification = kwargs['data'].modification
except KeyError:
modification = None
stream.write(self.version_string(ver, modification).encode("ascii"))
stream.write('\x0a'.encode("ascii"))
def get_size(self, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
return len(self.version_string(ver).encode("ascii")) + 1
@staticmethod
def version_string(version, modification=None):
"""Transforms version number into a version string.
>>> NifFormat.HeaderString.version_string(0x03000300)
'NetImmerse File Format, Version 3.03'
>>> NifFormat.HeaderString.version_string(0x03010000)
'NetImmerse File Format, Version 3.1'
>>> NifFormat.HeaderString.version_string(0x0A000100)
'NetImmerse File Format, Version 10.0.1.0'
>>> NifFormat.HeaderString.version_string(0x0A010000)
'Gamebryo File Format, Version 10.1.0.0'
>>> NifFormat.HeaderString.version_string(0x0A010000,
... modification="neosteam")
'NS'
>>> NifFormat.HeaderString.version_string(0x14020008,
... modification="ndoors")
'NDSNIF....@....@...., Version 20.2.0.8'
>>> NifFormat.HeaderString.version_string(0x14030009,
... modification="jmihs1")
'Joymaster HS1 Object Format - (JMI), Version 20.3.0.9'
"""
if version == -1 or version is None:
raise ValueError('No string for version %s.'%version)
if modification == "neosteam":
if version != 0x0A010000:
raise ValueError("NeoSteam must have version 0x0A010000.")
return "NS"
elif version <= 0x0A000102:
s = "NetImmerse"
else:
s = "Gamebryo"
if version == 0x03000300:
v = "3.03"
elif version <= 0x03010000:
v = "%i.%i"%((version >> 24) & 0xff, (version >> 16) & 0xff)
else:
v = "%i.%i.%i.%i"%((version >> 24) & 0xff, (version >> 16) & 0xff, (version >> 8) & 0xff, version & 0xff)
if not modification:
return "%s File Format, Version %s" % (s, v)
elif modification == "ndoors":
return "NDSNIF....@....@...., Version %s" % v
elif modification == "jmihs1":
return "Joymaster HS1 Object Format - (JMI), Version %s" % v
class FileVersion(BasicBase):
def get_value(self):
raise NotImplementedError
def set_value(self, value):
raise NotImplementedError
def __str__(self):
return 'x.x.x.x'
def get_size(self, **kwargs):
return 4
def get_hash(self, **kwargs):
return None
def read(self, stream, **kwargs):
modification = getattr(kwargs['data'], 'modification', None)
ver, = struct.unpack('<I', stream.read(4))
if (not modification) or modification == "jmihs1":
if ver != kwargs['data'].version:
raise ValueError(
"Invalid version number: "
"expected 0x%08X but got 0x%08X."
% (kwargs['data'].version, ver))
elif modification == "neosteam":
if ver != 0x08F35232:
raise ValueError(
"Invalid NeoSteam version number: "
"expected 0x%08X but got 0x%08X."
% (0x08F35232, ver))
elif modification == "ndoors":
if ver != 0x73615F67:
raise ValueError(
"Invalid Ndoors version number: "
"expected 0x%08X but got 0x%08X."
% (0x73615F67, ver))
else:
raise ValueError(
"unknown modification: '%s'" % modification)
def write(self, stream, **kwargs):
modification = getattr(kwargs['data'], 'modification', None)
if (not modification) or modification == "jmihs1":
stream.write(struct.pack('<I', kwargs['data'].version))
elif modification == "neosteam":
stream.write(struct.pack('<I', 0x08F35232))
elif modification == "ndoors":
stream.write(struct.pack('<I', 0x73615F67))
else:
raise ValueError(
"unknown modification: '%s'" % modification)
def get_detail_display(self):
return 'x.x.x.x'
class ShortString(BasicBase):
"""Another type for strings."""
def __init__(self, **kwargs):
BasicBase.__init__(self, **kwargs)
self._value = ''.encode("ascii")
def get_value(self):
return self._value
def set_value(self, value):
val = pyffi.object_models.common._as_bytes(value)
if len(val) > 254:
raise ValueError('string too long')
self._value = val
def __str__(self):
return pyffi.object_models.common._as_str(self._value)
def get_size(self, **kwargs):
# length byte + string chars + zero byte
return len(self._value) + 2
def get_hash(self, **kwargs):
return self.get_value()
def read(self, stream, **kwargs):
n, = struct.unpack('<B', stream.read(1))
self._value = stream.read(n).rstrip('\x00'.encode("ascii"))
def write(self, stream, **kwargs):
stream.write(struct.pack('<B', len(self._value)+1))
stream.write(self._value)
stream.write('\x00'.encode("ascii"))
class string(SizedString):
_has_strings = True
def get_size(self, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
if ver >= 0x14010003:
return 4
else:
return 4 + len(self._value)
def read(self, stream, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
n, = struct.unpack('<i', stream.read(4))
if ver >= 0x14010003:
if n == -1:
self._value = ''.encode("ascii")
else:
try:
self._value = kwargs.get('string_list')[n]
except IndexError:
raise ValueError('string index too large (%i)'%n)
else:
if n > 10000: raise ValueError('string too long (0x%08X at 0x%08X)'%(n, stream.tell()))
self._value = stream.read(n)
def write(self, stream, **kwargs):
try:
ver = kwargs['data'].version
except KeyError:
ver = -1
if ver >= 0x14010003:
if not self._value:
stream.write(struct.pack('<i', -1))
else:
try:
stream.write(struct.pack(
'<i', kwargs.get('string_list').index(self._value)))
except ValueError:
raise ValueError(
"string '%s' not in string list"%self._value)
else:
stream.write(struct.pack('<I', len(self._value)))
stream.write(self._value)
def get_strings(self, **kwargs):
if self._value:
return [self._value]
else:
return []
def get_hash(self, **kwargs):
if not kwargs.get('ignore_strings'):
return self.get_value()
# other types with internal implementation
class FilePath(string):
"""A file path."""
def get_hash(self, **kwargs):
"""Returns a case insensitive hash value."""
return self.get_value().lower()
class ByteArray(BasicBase):
"""Array (list) of bytes. Implemented as basic type to speed up reading
and also to prevent data to be dumped by __str__."""
def __init__(self, **kwargs):
BasicBase.__init__(self, **kwargs)
self.set_value("".encode()) # b'' for > py25
def get_value(self):
return self._value
def set_value(self, value):
self._value = pyffi.object_models.common._as_bytes(value)
def get_size(self, **kwargs):
return len(self._value) + 4
def get_hash(self, **kwargs):
return self._value.__hash__()
def read(self, stream, **kwargs):
size, = struct.unpack('<I', stream.read(4))
self._value = stream.read(size)
def write(self, stream, **kwargs):
stream.write(struct.pack('<I', len(self._value)))
stream.write(self._value)
def __str__(self):
return "< %i Bytes >" % len(self._value)
class ByteMatrix(BasicBase):
"""Matrix of bytes. Implemented as basic type to speed up reading
and to prevent data being dumped by __str__."""
def __init__(self, **kwargs):
BasicBase.__init__(self, **kwargs)
self.set_value([])
def get_value(self):
return self._value
def set_value(self, value):
assert(isinstance(value, list))
if value:
size1 = len(value[0])
for x in value:
# TODO fix this for py3k
#assert(isinstance(x, basestring))
assert(len(x) == size1)
self._value = value # should be a list of strings of bytes
def get_size(self, **kwargs):
if len(self._value) == 0:
return 8
else:
return len(self._value) * len(self._value[0]) + 8
def get_hash(self, **kwargs):
return tuple( x.__hash__() for x in self._value )
def read(self, stream, **kwargs):
size1, = struct.unpack('<I', stream.read(4))
size2, = struct.unpack('<I', stream.read(4))
self._value = []
for i in xrange(size2):
self._value.append(stream.read(size1))
def write(self, stream, **kwargs):
if self._value:
stream.write(struct.pack('<I', len(self._value[0])))
else:
stream.write(struct.pack('<I', 0))
stream.write(struct.pack('<I', len(self._value)))
for x in self._value:
stream.write(x)
def __str__(self):
size1 = len(self._value[0]) if self._value else 0
size2 = len(self._value)
return "< %ix%i Bytes >" % (size2, size1)
@classmethod
def vercondFilter(cls, expression):
if expression == "Version":
return "version"
elif expression == "User Version":
return "user_version"
elif expression == "User Version 2":
return "user_version2"
ver = cls.version_number(expression)
if ver < 0:
# not supported?
raise ValueError(
"cannot recognize version expression '%s'" % expression)
else:
return ver
@staticmethod
def version_number(version_str):
"""Converts version string into an integer.
:param version_str: The version string.
:type version_str: str
:return: A version integer.
>>> hex(NifFormat.version_number('3.14.15.29'))
'0x30e0f1d'
>>> hex(NifFormat.version_number('1.2'))
'0x1020000'
>>> hex(NifFormat.version_number('3.03'))
'0x3000300'
>>> hex(NifFormat.version_number('NS'))
'0xa010000'
"""
# 3.03 case is special
if version_str == '3.03':
return 0x03000300
# NS (neosteam) case is special
if version_str == 'NS':
return 0x0A010000
try:
ver_list = [int(x) for x in version_str.split('.')]
except ValueError:
return -1 # version not supported (i.e. version_str '10.0.1.3a' would trigger this)
if len(ver_list) > 4 or len(ver_list) < 1:
return -1 # version not supported
for ver_digit in ver_list:
if (ver_digit | 0xff) > 0xff:
return -1 # version not supported
while len(ver_list) < 4: ver_list.append(0)
return (ver_list[0] << 24) + (ver_list[1] << 16) + (ver_list[2] << 8) + ver_list[3]
# exceptions
class NifError(Exception):
"""Standard nif exception class."""
pass
class Data(pyffi.object_models.FileFormat.Data):
"""A class to contain the actual nif data.
Note that L{header} and L{blocks} are not automatically kept
in sync with the rest of the nif data, but they are
resynchronized when calling L{write}.
:ivar version: The nif version.
:type version: ``int``
:ivar user_version: The nif user version.
:type user_version: ``int``
:ivar user_version2: The nif user version 2.
:type user_version2: ``int``
:ivar roots: List of root blocks.
:type roots: ``list`` of L{NifFormat.NiObject}
:ivar header: The nif header.
:type header: L{NifFormat.Header}
:ivar blocks: List of blocks.
:type blocks: ``list`` of L{NifFormat.NiObject}
:ivar modification: Neo Steam ("neosteam") or Ndoors ("ndoors") or Joymaster Interactive Howling Sword ("jmihs1") style nif?
:type modification: ``str``
"""
class VersionUInt(pyffi.object_models.common.UInt):
def set_value(self, value):
if value is None:
self._value = None
else:
pyffi.object_models.common.UInt.set_value(self, value)
def __str__(self):
if self._value is None:
return "None"
else:
return "0x%08X" % self.get_value()
def get_detail_display(self):
return self.__str__()
def __init__(self, version=0x04000002, user_version=0, user_version2=0):
"""Initialize nif data. By default, this creates an empty
nif document of the given version and user version.
:param version: The version.
:type version: ``int``
:param user_version: The user version.
:type user_version: ``int``
"""
# the version numbers are stored outside the header structure
self._version_value_ = self.VersionUInt()
self._version_value_.set_value(version)
self._user_version_value_ = self.VersionUInt()
self._user_version_value_.set_value(user_version)
self._user_version_2_value_ = self.VersionUInt()
self._user_version_2_value_.set_value(user_version2)
# create new header
self.header = NifFormat.Header()
# empty list of root blocks (this encodes the footer)
self.roots = []
# empty list of blocks
self.blocks = []
# not a neosteam or ndoors nif
self.modification = None
def _getVersion(self):
return self._version_value_.get_value()
def _setVersion(self, value):
self._version_value_.set_value(value)
def _getUserVersion(self):
return self._user_version_value_.get_value()
def _setUserVersion(self, value):
self._user_version_value_.set_value(value)
def _getUserVersion2(self):
return self._user_version_2_value_.get_value()
def _setUserVersion2(self, value):
self._user_version_2_value_.set_value(value)
version = property(_getVersion, _setVersion)
user_version = property(_getUserVersion, _setUserVersion)
user_version2 = property(_getUserVersion2, _setUserVersion2)
# new functions
def inspectVersionOnly(self, stream):
"""This function checks the version only, and is faster
than the usual inspect function (which reads the full
header). Sets the L{version} and L{user_version} instance
variables if the stream contains a valid nif file.
Call this function if you simply wish to check that a file is
a nif file without having to parse even the header.
:raise ``ValueError``: If the stream does not contain a nif file.
:param stream: The stream from which to read.
:type stream: ``file``
"""
pos = stream.tell()
try:
s = stream.readline(64).rstrip()
finally:
stream.seek(pos)
self.modification = None
if s.startswith("NetImmerse File Format, Version ".encode("ascii")):
version_str = s[32:].decode("ascii")
elif s.startswith("Gamebryo File Format, Version ".encode("ascii")):
version_str = s[30:].decode("ascii")
elif s.startswith("NS".encode("ascii")):
# neosteam
version_str = "NS"
self.modification = "neosteam"
elif s.startswith("NDSNIF....@....@...., Version ".encode("ascii")):
version_str = s[30:].decode("ascii")
self.modification = "ndoors"
elif s.startswith("Joymaster HS1 Object Format - (JMI), Version ".encode("ascii")):
version_str = s[45:].decode("ascii")
self.modification = "jmihs1"
else:
raise ValueError("Not a nif file.")
try:
ver = NifFormat.version_number(version_str)
except:
raise ValueError("Nif version %s not supported." % version_str)
if not ver in NifFormat.versions.values():
raise ValueError("Nif version %s not supported." % version_str)
# check version integer and user version
userver = 0
userver2 = 0
if ver >= 0x0303000D:
ver_int = None
try:
stream.readline(64)
ver_int, = struct.unpack('<I', stream.read(4))
# neosteam and ndoors have a special version integer
if (not self.modification) or self.modification == "jmihs1":
if ver_int != ver:
raise ValueError(
"Corrupted nif file: header version string %s"
" does not correspond with header version field"
" 0x%08X." % (version_str, ver_int))
elif self.modification == "neosteam":
if ver_int != 0x08F35232:
raise ValueError(
"Corrupted nif file: invalid NeoSteam version.")
elif self.modification == "ndoors":
if ver_int != 0x73615F67:
raise ValueError(
"Corrupted nif file: invalid Ndoors version.")
if ver >= 0x14000004:
endian_type, = struct.unpack('<B', stream.read(1))
if endian_type == 0:
# big endian!
raise ValueError(
"Big endian nifs not supported.")
if ver >= 0x0A010000:
userver, = struct.unpack('<I', stream.read(4))
if userver in (10, 11):
stream.read(4) # number of blocks
userver2, = struct.unpack('<I', stream.read(4))
finally:
stream.seek(pos)
self.version = ver
self.user_version = userver
self.user_version2 = userver2
# GlobalNode
def get_global_child_nodes(self, edge_filter=EdgeFilter()):
return (root for root in self.roots)
# DetailNode
def replace_global_node(self, oldbranch, newbranch,
edge_filter=EdgeFilter()):
for i, root in enumerate(self.roots):
if root is oldbranch:
self.roots[i] = newbranch
else:
root.replace_global_node(oldbranch, newbranch,
edge_filter=edge_filter)
def get_detail_child_nodes(self, edge_filter=EdgeFilter()):
yield self._version_value_
yield self._user_version_value_
yield self._user_version_2_value_
yield self.header
def get_detail_child_names(self, edge_filter=EdgeFilter()):
yield "Version"
yield "User Version"
yield "User Version 2"
yield "Header"
# overriding pyffi.object_models.FileFormat.Data methods
def inspect(self, stream):
"""Quickly checks whether the stream appears to contain
nif data, and read the nif header. Resets stream to original position.
Call this function if you only need to inspect the header of the nif.
:param stream: The file to inspect.
:type stream: ``file``
"""
pos = stream.tell()
try:
self.inspectVersionOnly(stream)
self.header.read(stream, data=self)
finally:
stream.seek(pos)
def read(self, stream, verbose=0):
"""Read a nif file. Does not reset stream position.
:param stream: The stream from which to read.
:type stream: ``file``
:param verbose: The level of verbosity.
:type verbose: ``int``
"""
logger = logging.getLogger("pyffi.nif.data")
# read header
logger.debug("Reading header at 0x%08X" % stream.tell())
self.inspectVersionOnly(stream)
logger.debug("Version 0x%08X" % self.version)
self.header.read(stream, data=self)
# list of root blocks
# for versions < 3.3.0.13 this list is updated through the
# "Top Level Object" string while reading the blocks
# for more recent versions, this list is updated at the end when the
# footer is read
self.roots = []
# read the blocks
link_stack = [] # list of indices, as they are added to the stack
string_list = [s for s in self.header.strings]
block_dct = {} # maps block index to actual block
self.blocks = [] # records all blocks as read from file in order
block_num = 0 # the current block numner
while True:
if self.version < 0x0303000D:
# check if this is a 'Top Level Object'
pos = stream.tell()
top_level_str = NifFormat.SizedString()
top_level_str.read(stream)
top_level_str = str(top_level_str)
if top_level_str == "Top Level Object":
is_root = True
else:
is_root = False
stream.seek(pos)
else:
# signal as no root for now, roots are added when the footer
# is read
is_root = False
# get block name
if self.version >= 0x05000001:
# note the 0xfff mask: required for the NiPhysX blocks
block_type = self.header.block_types[
self.header.block_type_index[block_num] & 0xfff]
block_type = block_type.decode("ascii")
# handle data stream classes
if block_type.startswith("NiDataStream\x01"):
block_type, data_stream_usage, data_stream_access = block_type.split("\x01")
data_stream_usage = int(data_stream_usage)
data_stream_access = int(data_stream_access)
# read dummy integer
# bhk blocks are *not* preceeded by a dummy
if self.version <= 0x0A01006A and not block_type.startswith("bhk"):
dummy, = struct.unpack('<I', stream.read(4))
if dummy != 0:
raise NifFormat.NifError(
'non-zero block tag 0x%08X at 0x%08X)'
%(dummy, stream.tell()))
else:
block_type = NifFormat.SizedString()
block_type.read(stream)
block_type = block_type.get_value().decode("ascii")
# get the block index
if self.version >= 0x0303000D:
# for these versions the block index is simply the block number
block_index = block_num
else:
# earlier versions
# the number of blocks is not in the header
# and a special block type string marks the end of the file
if block_type == "End Of File": break
# read the block index, which is probably the memory
# location of the object when it was written to
# memory
else:
block_index, = struct.unpack('<I', stream.read(4))
if block_index in block_dct:
raise NifFormat.NifError(
'duplicate block index (0x%08X at 0x%08X)'
%(block_index, stream.tell()))
# create the block
try:
block = getattr(NifFormat, block_type)()
except AttributeError:
raise ValueError(
"Unknown block type '%s'." % block_type)
logger.debug("Reading %s block at 0x%08X"
% (block_type, stream.tell()))
# read the block
try:
block.read(
stream,
data=self,
link_stack=link_stack, string_list=string_list)
except:
logger.exception("Reading %s failed" % block.__class__)
#logger.error("link stack: %s" % link_stack)
#logger.error("block that failed:")
#logger.error("%s" % block)
raise
# complete NiDataStream data
if block_type == "NiDataStream":
block.usage = data_stream_usage
block.access.from_int(data_stream_access)
# store block index
block_dct[block_index] = block
self.blocks.append(block)
# check block size
if self.version >= 0x14020007:
logger.debug("Checking block size")
calculated_size = block.get_size(data=self)
if calculated_size != self.header.block_size[block_num]:
extra_size = self.header.block_size[block_num] - calculated_size
logger.error(
"Block size check failed: corrupt nif file "
"or bad nif.xml?")
logger.error("Skipping %i bytes in %s"
% (extra_size, block.__class__.__name__))
# skip bytes that were missed
stream.seek(extra_size, 1)
# add block to roots if flagged as such
if is_root:
self.roots.append(block)
# check if we are done
block_num += 1
if self.version >= 0x0303000D:
if block_num >= self.header.num_blocks:
break
# read footer
ftr = NifFormat.Footer()
ftr.read(
stream,
data=self,
link_stack = link_stack)
# check if we are at the end of the file
if stream.read(1):
logger.error(
'End of file not reached: corrupt nif file?')
# fix links in blocks and footer (header has no links)
for block in self.blocks:
block.fix_links(
data=self,
block_dct = block_dct, link_stack = link_stack)
ftr.fix_links(
data=self,
block_dct = block_dct, link_stack= link_stack)
# the link stack should be empty now
if link_stack:
raise NifFormat.NifError('not all links have been popped from the stack (bug?)')
# add root objects in footer to roots list
if self.version >= 0x0303000D:
for root in ftr.roots:
self.roots.append(root)
def write(self, stream, verbose=0):
"""Write a nif file. The L{header} and the L{blocks} are recalculated
from the tree at L{roots} (e.g. list of block types, number of blocks,
list of block types, list of strings, list of block sizes etc.).
:param stream: The stream to which to write.
:type stream: file
:param verbose: The level of verbosity.
:type verbose: int
"""
logger = logging.getLogger("pyffi.nif.data")
# set up index and type dictionary
self.blocks = [] # list of all blocks to be written
block_index_dct = {} # maps block to block index
block_type_list = [] # list of all block type strings
block_type_dct = {} # maps block to block type string index
string_list = []
for root in self.roots:
self._makeBlockList(root,
block_index_dct,
block_type_list, block_type_dct)
for block in root.tree():
string_list.extend(
block.get_strings(
data=self))
string_list = list(set(string_list)) # ensure unique elements
#print(string_list) # debug
self.header.user_version = self.user_version # TODO dedicated type for user_version similar to FileVersion
# for oblivion CS; apparently this is the version of the bhk blocks
self.header.user_version_2 = self.user_version2
self.header.num_blocks = len(self.blocks)
self.header.num_block_types = len(block_type_list)
self.header.block_types.update_size()
for i, block_type in enumerate(block_type_list):
self.header.block_types[i] = block_type
self.header.block_type_index.update_size()
for i, block in enumerate(self.blocks):
self.header.block_type_index[i] = block_type_dct[block]
self.header.num_strings = len(string_list)
if string_list:
self.header.max_string_length = max([len(s) for s in string_list])
else:
self.header.max_string_length = 0
self.header.strings.update_size()
for i, s in enumerate(string_list):
self.header.strings[i] = s
self.header.block_size.update_size()
for i, block in enumerate(self.blocks):
self.header.block_size[i] = block.get_size(data=self)
#if verbose >= 2:
# print(hdr)
# set up footer
ftr = NifFormat.Footer()
ftr.num_roots = len(self.roots)
ftr.roots.update_size()
for i, root in enumerate(self.roots):
ftr.roots[i] = root
# write the file
logger.debug("Writing header")
#logger.debug("%s" % self.header)
self.header.write(
stream,
data=self,
block_index_dct = block_index_dct)
for block in self.blocks:
# signal top level object if block is a root object
if self.version < 0x0303000D and block in self.roots:
s = NifFormat.SizedString()
s.set_value("Top Level Object")
s.write(stream)
if self.version >= 0x05000001:
if self.version <= 0x0A01006A:
# write zero dummy separator
stream.write('\x00\x00\x00\x00'.encode("ascii"))
else:
# write block type string
s = NifFormat.SizedString()
assert(block_type_list[block_type_dct[block]]
== block.__class__.__name__) # debug
s.set_value(block.__class__.__name__)
s.write(stream)
# write block index
logger.debug("Writing %s block" % block.__class__.__name__)
if self.version < 0x0303000D:
stream.write(struct.pack('<i', block_index_dct[block]))
# write block
block.write(
stream,
data=self,
block_index_dct = block_index_dct, string_list = string_list)
if self.version < 0x0303000D:
s = NifFormat.SizedString()
s.set_value("End Of File")
s.write(stream)
ftr.write(
stream,
data=self,
block_index_dct = block_index_dct)
def _makeBlockList(
self, root, block_index_dct, block_type_list, block_type_dct):
"""This is a helper function for write to set up the list of all blocks,
the block index map, and the block type map.
:param root: The root block, whose tree is to be added to
the block list.
:type root: L{NifFormat.NiObject}
:param block_index_dct: Dictionary mapping blocks in self.blocks to
their block index.
:type block_index_dct: dict
:param block_type_list: List of all block types.
:type block_type_list: list of str
:param block_type_dct: Dictionary mapping blocks in self.blocks to
their block type index.
:type block_type_dct: dict
"""
def _blockChildBeforeParent(block):
"""Determine whether block comes before its parent or not, depending
on the block type.
@todo: Move to the L{NifFormat.Data} class.
:param block: The block to test.
:type block: L{NifFormat.NiObject}
:return: ``True`` if child should come first, ``False`` otherwise.
"""
return (isinstance(block, NifFormat.bhkRefObject)
and not isinstance(block, NifFormat.bhkConstraint))
# block already listed? if so, return
if root in self.blocks:
return
# add block type to block type dictionary
block_type = root.__class__.__name__
# special case: NiDataStream stores part of data in block type list
if block_type == "NiDataStream":
# XXX assumed here that root.access is version independent!!
block_type = "NiDataStream\x01%i\x01%i" % (root.usage,
root.access.to_int())
try:
block_type_dct[root] = block_type_list.index(block_type)
except ValueError:
block_type_dct[root] = len(block_type_list)
block_type_list.append(block_type)
# special case: add bhkConstraint entities before bhkConstraint
# (these are actually links, not refs)
if isinstance(root, NifFormat.bhkConstraint):
for entity in root.entities:
self._makeBlockList(
entity, block_index_dct, block_type_list, block_type_dct)
# add children that come before the block
for child in root.get_refs(data=self):
if _blockChildBeforeParent(child):
self._makeBlockList(
child, block_index_dct, block_type_list, block_type_dct)
# add the block
if self.version >= 0x0303000D:
block_index_dct[root] = len(self.blocks)
else:
block_index_dct[root] = id(root)
self.blocks.append(root)
# add children that come after the block
for child in root.get_refs(data=self):
if not _blockChildBeforeParent(child):
self._makeBlockList(
child, block_index_dct, block_type_list, block_type_dct)
# extensions of generated structures
class Footer:
def read(self, stream, **kwargs):
StructBase.read(self, stream, **kwargs)
modification = getattr(kwargs['data'], 'modification', None)
if modification == "neosteam":
extrabyte, = struct.unpack("<B", stream.read(1))
if extrabyte != 0:
raise ValueError(
"Expected trailing zero byte in footer, "
"but got %i instead." % extrabyte)
def write(self, stream, **kwargs):
StructBase.write(self, stream, **kwargs)
modification = getattr(kwargs['data'], 'modification', None)
if modification == "neosteam":
stream.write("\x00".encode("ascii"))
class Header:
def has_block_type(self, block_type):
"""Check if header has a particular block type.
:raise ``ValueError``: If number of block types is zero
(only nif versions 10.0.1.0 and up store block types
in header).
:param block_type: The block type.
:type block_type: L{NifFormat.NiObject}
:return: ``True`` if the header's list of block types has the given
block type, or a subclass of it. ``False`` otherwise.
:rtype: ``bool``
"""
# check if we can check the block types at all
if self.num_block_types == 0:
raise ValueError("header does not store any block types")
# quick first check, without hierarchy, using simple string comparisons
if block_type.__name__.encode() in self.block_types:
return True
# slower check, using isinstance
for data_block_type in self.block_types:
data_block_type = data_block_type.decode("ascii")
# NiDataStreams are special
if data_block_type.startswith("NiDataStream\x01"):
data_block_type = "NiDataStream"
if issubclass(getattr(NifFormat, data_block_type), block_type):
return True
# requested block type is not in nif
return False
class Matrix33:
def as_list(self):
"""Return matrix as 3x3 list."""
return [
[self.m_11, self.m_12, self.m_13],
[self.m_21, self.m_22, self.m_23],
[self.m_31, self.m_32, self.m_33]
]
def as_tuple(self):
"""Return matrix as 3x3 tuple."""
return (
(self.m_11, self.m_12, self.m_13),
(self.m_21, self.m_22, self.m_23),
(self.m_31, self.m_32, self.m_33)
)
def __str__(self):
return (
"[ %6.3f %6.3f %6.3f ]\n"
"[ %6.3f %6.3f %6.3f ]\n"
"[ %6.3f %6.3f %6.3f ]\n"
% (self.m_11, self.m_12, self.m_13,
self.m_21, self.m_22, self.m_23,
self.m_31, self.m_32, self.m_33))
def set_identity(self):
"""Set to identity matrix."""
self.m_11 = 1.0
self.m_12 = 0.0
self.m_13 = 0.0
self.m_21 = 0.0
self.m_22 = 1.0
self.m_23 = 0.0
self.m_31 = 0.0
self.m_32 = 0.0
self.m_33 = 1.0
def is_identity(self):
"""Return ``True`` if the matrix is close to identity."""
if (abs(self.m_11 - 1.0) > NifFormat.EPSILON
or abs(self.m_12) > NifFormat.EPSILON
or abs(self.m_13) > NifFormat.EPSILON
or abs(self.m_21) > NifFormat.EPSILON
or abs(self.m_22 - 1.0) > NifFormat.EPSILON
or abs(self.m_23) > NifFormat.EPSILON
or abs(self.m_31) > NifFormat.EPSILON
or abs(self.m_32) > NifFormat.EPSILON
or abs(self.m_33 - 1.0) > NifFormat.EPSILON):
return False
else:
return True
def get_copy(self):
"""Return a copy of the matrix."""
mat = NifFormat.Matrix33()
mat.m_11 = self.m_11
mat.m_12 = self.m_12
mat.m_13 = self.m_13
mat.m_21 = self.m_21
mat.m_22 = self.m_22
mat.m_23 = self.m_23
mat.m_31 = self.m_31
mat.m_32 = self.m_32
mat.m_33 = self.m_33
return mat
def get_transpose(self):
"""Get transposed of the matrix."""
mat = NifFormat.Matrix33()
mat.m_11 = self.m_11
mat.m_12 = self.m_21
mat.m_13 = self.m_31
mat.m_21 = self.m_12
mat.m_22 = self.m_22
mat.m_23 = self.m_32
mat.m_31 = self.m_13
mat.m_32 = self.m_23
mat.m_33 = self.m_33
return mat
def is_scale_rotation(self):
"""Returns true if the matrix decomposes nicely into scale * rotation."""
# NOTE: 0.01 instead of NifFormat.EPSILON to work around bad nif files
# calculate self * self^T
# this should correspond to
# (scale * rotation) * (scale * rotation)^T
# = scale^2 * rotation * rotation^T
# = scale^2 * 3x3 identity matrix
self_transpose = self.get_transpose()
mat = self * self_transpose
# off diagonal elements should be zero
if (abs(mat.m_12) + abs(mat.m_13)
+ abs(mat.m_21) + abs(mat.m_23)
+ abs(mat.m_31) + abs(mat.m_32)) > 0.01:
return False
# diagonal elements should be equal (to scale^2)
if abs(mat.m_11 - mat.m_22) + abs(mat.m_22 - mat.m_33) > 0.01:
return False
return True
def is_rotation(self):
"""Returns ``True`` if the matrix is a rotation matrix
(a member of SO(3))."""
# NOTE: 0.01 instead of NifFormat.EPSILON to work around bad nif files
if not self.is_scale_rotation():
return False
if abs(self.get_determinant() - 1.0) > 0.01:
return False
return True
def get_determinant(self):
"""Return determinant."""
return (self.m_11*self.m_22*self.m_33
+self.m_12*self.m_23*self.m_31
+self.m_13*self.m_21*self.m_32
-self.m_31*self.m_22*self.m_13
-self.m_21*self.m_12*self.m_33
-self.m_11*self.m_32*self.m_23)
def get_scale(self):
"""Gets the scale (assuming is_scale_rotation is true!)."""
scale = self.get_determinant()
if scale < 0:
return -((-scale)**(1.0/3.0))
else:
return scale**(1.0/3.0)
def get_scale_rotation(self):
"""Decompose the matrix into scale and rotation, where scale is a float
and rotation is a C{Matrix33}. Returns a pair (scale, rotation)."""
rot = self.get_copy()
scale = self.get_scale()
if abs(scale) < NifFormat.EPSILON:
raise ZeroDivisionError('scale is zero, unable to obtain rotation')
rot /= scale
return (scale, rot)
def set_scale_rotation(self, scale, rotation):
"""Compose the matrix as the product of scale * rotation."""
if not isinstance(scale, (float, int, long)):
raise TypeError('scale must be float')
if not isinstance(rotation, NifFormat.Matrix33):
raise TypeError('rotation must be Matrix33')
if not rotation.is_rotation():
raise ValueError('rotation must be rotation matrix')
self.m_11 = rotation.m_11 * scale
self.m_12 = rotation.m_12 * scale
self.m_13 = rotation.m_13 * scale
self.m_21 = rotation.m_21 * scale
self.m_22 = rotation.m_22 * scale
self.m_23 = rotation.m_23 * scale
self.m_31 = rotation.m_31 * scale
self.m_32 = rotation.m_32 * scale
self.m_33 = rotation.m_33 * scale
def get_scale_quat(self):
"""Decompose matrix into scale and quaternion."""
scale, rot = self.get_scale_rotation()
quat = NifFormat.Quaternion()
trace = 1.0 + rot.m_11 + rot.m_22 + rot.m_33
if trace > NifFormat.EPSILON:
s = (trace ** 0.5) * 2
quat.x = -( rot.m_32 - rot.m_23 ) / s
quat.y = -( rot.m_13 - rot.m_31 ) / s
quat.z = -( rot.m_21 - rot.m_12 ) / s
quat.w = 0.25 * s
elif rot.m_11 > max((rot.m_22, rot.m_33)):
s = (( 1.0 + rot.m_11 - rot.m_22 - rot.m_33 ) ** 0.5) * 2
quat.x = 0.25 * s
quat.y = (rot.m_21 + rot.m_12 ) / s
quat.z = (rot.m_13 + rot.m_31 ) / s
quat.w = -(rot.m_32 - rot.m_23 ) / s
elif rot.m_22 > rot.m_33:
s = (( 1.0 + rot.m_22 - rot.m_11 - rot.m_33 ) ** 0.5) * 2
quat.x = (rot.m_21 + rot.m_12 ) / s
quat.y = 0.25 * s
quat.z = (rot.m_32 + rot.m_23 ) / s
quat.w = -(rot.m_13 - rot.m_31 ) / s
else:
s = (( 1.0 + rot.m_33 - rot.m_11 - rot.m_22 ) ** 0.5) * 2
quat.x = (rot.m_13 + rot.m_31 ) / s
quat.y = (rot.m_32 + rot.m_23 ) / s
quat.z = 0.25 * s
quat.w = -(rot.m_21 - rot.m_12 ) / s
return scale, quat
def get_inverse(self):
"""Get inverse (assuming is_scale_rotation is true!)."""
# transpose inverts rotation but keeps the scale
# dividing by scale^2 inverts the scale as well
return self.get_transpose() / (self.m_11**2 + self.m_12**2 + self.m_13**2)
def __mul__(self, rhs):
if isinstance(rhs, (float, int, long)):
mat = NifFormat.Matrix33()
mat.m_11 = self.m_11 * rhs
mat.m_12 = self.m_12 * rhs
mat.m_13 = self.m_13 * rhs
mat.m_21 = self.m_21 * rhs
mat.m_22 = self.m_22 * rhs
mat.m_23 = self.m_23 * rhs
mat.m_31 = self.m_31 * rhs
mat.m_32 = self.m_32 * rhs
mat.m_33 = self.m_33 * rhs
return mat
elif isinstance(rhs, NifFormat.Vector3):
raise TypeError(
"matrix*vector not supported; "
"please use left multiplication (vector*matrix)")
elif isinstance(rhs, NifFormat.Matrix33):
mat = NifFormat.Matrix33()
mat.m_11 = self.m_11 * rhs.m_11 + self.m_12 * rhs.m_21 + self.m_13 * rhs.m_31
mat.m_12 = self.m_11 * rhs.m_12 + self.m_12 * rhs.m_22 + self.m_13 * rhs.m_32
mat.m_13 = self.m_11 * rhs.m_13 + self.m_12 * rhs.m_23 + self.m_13 * rhs.m_33
mat.m_21 = self.m_21 * rhs.m_11 + self.m_22 * rhs.m_21 + self.m_23 * rhs.m_31
mat.m_22 = self.m_21 * rhs.m_12 + self.m_22 * rhs.m_22 + self.m_23 * rhs.m_32
mat.m_23 = self.m_21 * rhs.m_13 + self.m_22 * rhs.m_23 + self.m_23 * rhs.m_33
mat.m_31 = self.m_31 * rhs.m_11 + self.m_32 * rhs.m_21 + self.m_33 * rhs.m_31
mat.m_32 = self.m_31 * rhs.m_12 + self.m_32 * rhs.m_22 + self.m_33 * rhs.m_32
mat.m_33 = self.m_31 * rhs.m_13 + self.m_32 * rhs.m_23 + self.m_33 * rhs.m_33
return mat
else:
raise TypeError(
"do not know how to multiply Matrix33 with %s"%rhs.__class__)
def __div__(self, rhs):
if isinstance(rhs, (float, int, long)):
mat = NifFormat.Matrix33()
mat.m_11 = self.m_11 / rhs
mat.m_12 = self.m_12 / rhs
mat.m_13 = self.m_13 / rhs
mat.m_21 = self.m_21 / rhs
mat.m_22 = self.m_22 / rhs
mat.m_23 = self.m_23 / rhs
mat.m_31 = self.m_31 / rhs
mat.m_32 = self.m_32 / rhs
mat.m_33 = self.m_33 / rhs
return mat
else:
raise TypeError(
"do not know how to divide Matrix33 by %s"%rhs.__class__)
# py3k
__truediv__ = __div__
def __rmul__(self, lhs):
if isinstance(lhs, (float, int, long)):
return self * lhs # commutes
else:
raise TypeError(
"do not know how to multiply %s with Matrix33"%lhs.__class__)
def __eq__(self, mat):
if not isinstance(mat, NifFormat.Matrix33):
raise TypeError(
"do not know how to compare Matrix33 and %s"%mat.__class__)
if (abs(self.m_11 - mat.m_11) > NifFormat.EPSILON
or abs(self.m_12 - mat.m_12) > NifFormat.EPSILON
or abs(self.m_13 - mat.m_13) > NifFormat.EPSILON
or abs(self.m_21 - mat.m_21) > NifFormat.EPSILON
or abs(self.m_22 - mat.m_22) > NifFormat.EPSILON
or abs(self.m_23 - mat.m_23) > NifFormat.EPSILON
or abs(self.m_31 - mat.m_31) > NifFormat.EPSILON
or abs(self.m_32 - mat.m_32) > NifFormat.EPSILON
or abs(self.m_33 - mat.m_33) > NifFormat.EPSILON):
return False
return True
def __ne__(self, mat):
return not self.__eq__(mat)
def __sub__(self, x):
if isinstance(x, (NifFormat.Matrix33)):
m = NifFormat.Matrix33()
m.m_11 = self.m_11 - x.m_11
m.m_12 = self.m_12 - x.m_12
m.m_13 = self.m_13 - x.m_13
m.m_21 = self.m_21 - x.m_21
m.m_22 = self.m_22 - x.m_22
m.m_23 = self.m_23 - x.m_23
m.m_31 = self.m_31 - x.m_31
m.m_32 = self.m_32 - x.m_32
m.m_33 = self.m_33 - x.m_33
return m
elif isinstance(x, (int, long, float)):
m = NifFormat.Matrix33()
m.m_11 = self.m_11 - x
m.m_12 = self.m_12 - x
m.m_13 = self.m_13 - x
m.m_21 = self.m_21 - x
m.m_22 = self.m_22 - x
m.m_23 = self.m_23 - x
m.m_31 = self.m_31 - x
m.m_32 = self.m_32 - x
m.m_33 = self.m_33 - x
return m
else:
raise TypeError("do not know how to substract Matrix33 and %s"
% x.__class__)
def sup_norm(self):
"""Calculate supremum norm of matrix (maximum absolute value of all
entries)."""
return max(max(abs(elem) for elem in row)
for row in self.as_list())
class Vector3:
def as_list(self):
return [self.x, self.y, self.z]
def as_tuple(self):
return (self.x, self.y, self.z)
def norm(self):
return (self.x*self.x + self.y*self.y + self.z*self.z) ** 0.5
def normalize(self):
norm = self.norm()
if norm < NifFormat.EPSILON:
raise ZeroDivisionError('cannot normalize vector %s'%self)
self.x /= norm
self.y /= norm
self.z /= norm
def normalized(self):
vec = self.get_copy()
vec.normalize()
return vec
def get_copy(self):
v = NifFormat.Vector3()
v.x = self.x
v.y = self.y
v.z = self.z
return v
def __str__(self):
return "[ %6.3f %6.3f %6.3f ]"%(self.x, self.y, self.z)
def __mul__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.Vector3()
v.x = self.x * x
v.y = self.y * x
v.z = self.z * x
return v
elif isinstance(x, NifFormat.Vector3):
return self.x * x.x + self.y * x.y + self.z * x.z
elif isinstance(x, NifFormat.Matrix33):
v = NifFormat.Vector3()
v.x = self.x * x.m_11 + self.y * x.m_21 + self.z * x.m_31
v.y = self.x * x.m_12 + self.y * x.m_22 + self.z * x.m_32
v.z = self.x * x.m_13 + self.y * x.m_23 + self.z * x.m_33
return v
elif isinstance(x, NifFormat.Matrix44):
return self * x.get_matrix_33() + x.get_translation()
else:
raise TypeError("do not know how to multiply Vector3 with %s"%x.__class__)
def __rmul__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.Vector3()
v.x = x * self.x
v.y = x * self.y
v.z = x * self.z
return v
else:
raise TypeError("do not know how to multiply %s and Vector3"%x.__class__)
def __div__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.Vector3()
v.x = self.x / x
v.y = self.y / x
v.z = self.z / x
return v
else:
raise TypeError("do not know how to divide Vector3 and %s"%x.__class__)
# py3k
__truediv__ = __div__
def __add__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.Vector3()
v.x = self.x + x
v.y = self.y + x
v.z = self.z + x
return v
elif isinstance(x, NifFormat.Vector3):
v = NifFormat.Vector3()
v.x = self.x + x.x
v.y = self.y + x.y
v.z = self.z + x.z
return v
else:
raise TypeError("do not know how to add Vector3 and %s"%x.__class__)
def __radd__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.Vector3()
v.x = x + self.x
v.y = x + self.y
v.z = x + self.z
return v
else:
raise TypeError("do not know how to add %s and Vector3"%x.__class__)
def __sub__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.Vector3()
v.x = self.x - x
v.y = self.y - x
v.z = self.z - x
return v
elif isinstance(x, NifFormat.Vector3):
v = NifFormat.Vector3()
v.x = self.x - x.x
v.y = self.y - x.y
v.z = self.z - x.z
return v
else:
raise TypeError("do not know how to substract Vector3 and %s"%x.__class__)
def __rsub__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.Vector3()
v.x = x - self.x
v.y = x - self.y
v.z = x - self.z
return v
else:
raise TypeError("do not know how to substract %s and Vector3"%x.__class__)
def __neg__(self):
v = NifFormat.Vector3()
v.x = -self.x
v.y = -self.y
v.z = -self.z
return v
# cross product
def crossproduct(self, x):
if isinstance(x, NifFormat.Vector3):
v = NifFormat.Vector3()
v.x = self.y*x.z - self.z*x.y
v.y = self.z*x.x - self.x*x.z
v.z = self.x*x.y - self.y*x.x
return v
else:
raise TypeError("do not know how to calculate crossproduct of Vector3 and %s"%x.__class__)
def __eq__(self, x):
if isinstance(x, type(None)):
return False
if not isinstance(x, NifFormat.Vector3):
raise TypeError("do not know how to compare Vector3 and %s"%x.__class__)
if abs(self.x - x.x) > NifFormat.EPSILON: return False
if abs(self.y - x.y) > NifFormat.EPSILON: return False
if abs(self.z - x.z) > NifFormat.EPSILON: return False
return True
def __ne__(self, x):
return not self.__eq__(x)
class Vector4:
"""
>>> from pyffi.formats.nif import NifFormat
>>> vec = NifFormat.Vector4()
>>> vec.x = 1.0
>>> vec.y = 2.0
>>> vec.z = 3.0
>>> vec.w = 4.0
>>> print(vec)
[ 1.000 2.000 3.000 4.000 ]
>>> vec.as_list()
[1.0, 2.0, 3.0, 4.0]
>>> vec.as_tuple()
(1.0, 2.0, 3.0, 4.0)
>>> print(vec.get_vector_3())
[ 1.000 2.000 3.000 ]
>>> vec2 = NifFormat.Vector4()
>>> vec == vec2
False
>>> vec2.x = 1.0
>>> vec2.y = 2.0
>>> vec2.z = 3.0
>>> vec2.w = 4.0
>>> vec == vec2
True
"""
def as_list(self):
return [self.x, self.y, self.z, self.w]
def as_tuple(self):
return (self.x, self.y, self.z, self.w)
def get_copy(self):
v = NifFormat.Vector4()
v.x = self.x
v.y = self.y
v.z = self.z
v.w = self.w
return v
def get_vector_3(self):
v = NifFormat.Vector3()
v.x = self.x
v.y = self.y
v.z = self.z
return v
def __str__(self):
return "[ %6.3f %6.3f %6.3f %6.3f ]"%(self.x, self.y, self.z, self.w)
def __eq__(self, rhs):
if isinstance(rhs, type(None)):
return False
if not isinstance(rhs, NifFormat.Vector4):
raise TypeError(
"do not know how to compare Vector4 and %s" % rhs.__class__)
if abs(self.x - rhs.x) > NifFormat.EPSILON: return False
if abs(self.y - rhs.y) > NifFormat.EPSILON: return False
if abs(self.z - rhs.z) > NifFormat.EPSILON: return False
if abs(self.w - rhs.w) > NifFormat.EPSILON: return False
return True
def __ne__(self, rhs):
return not self.__eq__(rhs)
class SkinPartition:
def get_triangles(self):
"""Get list of triangles of this partition.
"""
# strips?
if self.num_strips:
for tri in pyffi.utils.tristrip.triangulate(self.strips):
yield tri
# no strips, do triangles
else:
for tri in self.triangles:
yield (tri.v_1, tri.v_2, tri.v_3)
def get_mapped_triangles(self):
"""Get list of triangles of this partition (mapping into the
geometry data vertex list).
"""
for tri in self.get_triangles():
yield tuple(self.vertex_map[v_index] for v_index in tri)
class bhkBoxShape:
def apply_scale(self, scale):
"""Apply scale factor C{scale} on data."""
# apply scale on dimensions
self.dimensions.x *= scale
self.dimensions.y *= scale
self.dimensions.z *= scale
self.minimum_size *= scale
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center, and inertia tensor."""
# the dimensions describe half the size of the box in each dimension
# so the length of a single edge is dimension.dir * 2
mass, inertia = pyffi.utils.inertia.getMassInertiaBox(
(self.dimensions.x * 2, self.dimensions.y * 2, self.dimensions.z * 2),
density = density, solid = solid)
return mass, (0,0,0), inertia
class bhkCapsuleShape:
def apply_scale(self, scale):
"""Apply scale factor <scale> on data."""
# apply scale on dimensions
self.radius *= scale
self.radius_1 *= scale
self.radius_2 *= scale
self.first_point.x *= scale
self.first_point.y *= scale
self.first_point.z *= scale
self.second_point.x *= scale
self.second_point.y *= scale
self.second_point.z *= scale
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center, and inertia tensor."""
# (assumes self.radius == self.radius_1 == self.radius_2)
length = (self.first_point - self.second_point).norm()
mass, inertia = pyffi.utils.inertia.getMassInertiaCapsule(
radius = self.radius, length = length,
density = density, solid = solid)
# now fix inertia so it is expressed in the right coordinates
# need a transform that maps (0,0,length/2) on (second - first) / 2
# and (0,0,-length/2) on (first - second)/2
vec1 = ((self.second_point - self.first_point) / length).as_tuple()
# find an orthogonal vector to vec1
index = min(enumerate(vec1), key=lambda val: abs(val[1]))[0]
vec2 = vecCrossProduct(vec1, tuple((1 if i == index else 0)
for i in xrange(3)))
vec2 = vecscalarMul(vec2, 1/vecNorm(vec2))
# find an orthogonal vector to vec1 and vec2
vec3 = vecCrossProduct(vec1, vec2)
# get transform matrix
transform_transposed = (vec2, vec3, vec1) # this is effectively the transposed of our transform
transform = matTransposed(transform_transposed)
# check the result (debug)
assert(vecDistance(matvecMul(transform, (0,0,1)), vec1) < 0.0001)
assert(abs(matDeterminant(transform) - 1) < 0.0001)
# transform the inertia tensor
inertia = matMul(matMul(transform_transposed, inertia), transform)
return (mass,
((self.first_point + self.second_point) * 0.5).as_tuple(),
inertia)
class bhkConstraint:
def get_transform_a_b(self, parent):
"""Returns the transform of the first entity relative to the second
entity. Root is simply a nif block that is a common parent to both
blocks."""
# check entities
if self.num_entities != 2:
raise ValueError(
"cannot get tranform for constraint "
"that hasn't exactly 2 entities")
# find transform of entity A relative to entity B
# find chains from parent to A and B entities
chainA = parent.find_chain(self.entities[0])
chainB = parent.find_chain(self.entities[1])
# validate the chains
assert(isinstance(chainA[-1], NifFormat.bhkRigidBody))
assert(isinstance(chainA[-2], NifFormat.NiCollisionObject))
assert(isinstance(chainA[-3], NifFormat.NiNode))
assert(isinstance(chainB[-1], NifFormat.bhkRigidBody))
assert(isinstance(chainB[-2], NifFormat.NiCollisionObject))
assert(isinstance(chainB[-3], NifFormat.NiNode))
# return the relative transform
return (chainA[-3].get_transform(relative_to = parent)
* chainB[-3].get_transform(relative_to = parent).get_inverse())
class bhkConvexVerticesShape:
def apply_scale(self, scale):
"""Apply scale factor on data."""
if abs(scale - 1.0) < NifFormat.EPSILON: return
for v in self.vertices:
v.x *= scale
v.y *= scale
v.z *= scale
for n in self.normals:
n.w *= scale
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center, and inertia tensor."""
# first find an enumeration of all triangles making up the convex shape
vertices, triangles = pyffi.utils.quickhull.qhull3d(
[vert.as_tuple() for vert in self.vertices])
# now calculate mass, center, and inertia
return pyffi.utils.inertia.get_mass_center_inertia_polyhedron(
vertices, triangles, density = density, solid = solid)
class bhkLimitedHingeConstraint:
def apply_scale(self, scale):
"""Scale data."""
# apply scale on transform
self.limited_hinge.pivot_a.x *= scale
self.limited_hinge.pivot_a.y *= scale
self.limited_hinge.pivot_a.z *= scale
self.limited_hinge.pivot_b.x *= scale
self.limited_hinge.pivot_b.y *= scale
self.limited_hinge.pivot_b.z *= scale
def update_a_b(self, parent):
"""Update the B data from the A data. The parent argument is simply a
common parent to the entities."""
self.limited_hinge.update_a_b(self.get_transform_a_b(parent))
class bhkListShape:
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return center of gravity and area."""
subshapes_mci = [ subshape.get_mass_center_inertia(density = density,
solid = solid)
for subshape in self.sub_shapes ]
total_mass = 0
total_center = (0, 0, 0)
total_inertia = ((0, 0, 0), (0, 0, 0), (0, 0, 0))
for mass, center, inertia in subshapes_mci:
total_mass += mass
total_center = vecAdd(total_center,
vecscalarMul(center, mass / total_mass))
total_inertia = matAdd(total_inertia, inertia)
return total_mass, total_center, total_inertia
def add_shape(self, shape, front = False):
"""Add shape to list."""
# check if it's already there
if shape in self.sub_shapes: return
# increase number of shapes
num_shapes = self.num_sub_shapes
self.num_sub_shapes = num_shapes + 1
self.sub_shapes.update_size()
# add the shape
if not front:
self.sub_shapes[num_shapes] = shape
else:
for i in xrange(num_shapes, 0, -1):
self.sub_shapes[i] = self.sub_shapes[i-1]
self.sub_shapes[0] = shape
# expand list of unknown ints as well
self.num_unknown_ints = num_shapes + 1
self.unknown_ints.update_size()
def remove_shape(self, shape):
"""Remove a shape from the shape list."""
# get list of shapes excluding the shape to remove
shapes = [s for s in self.sub_shapes if s != shape]
# set sub_shapes to this list
self.num_sub_shapes = len(shapes)
self.sub_shapes.update_size()
for i, s in enumerate(shapes):
self.sub_shapes[i] = s
# update unknown ints
self.num_unknown_ints = len(shapes)
self.unknown_ints.update_size()
class bhkMalleableConstraint:
def apply_scale(self, scale):
"""Scale data."""
# apply scale on transform
self.ragdoll.pivot_a.x *= scale
self.ragdoll.pivot_a.y *= scale
self.ragdoll.pivot_a.z *= scale
self.ragdoll.pivot_b.x *= scale
self.ragdoll.pivot_b.y *= scale
self.ragdoll.pivot_b.z *= scale
self.limited_hinge.pivot_a.x *= scale
self.limited_hinge.pivot_a.y *= scale
self.limited_hinge.pivot_a.z *= scale
self.limited_hinge.pivot_b.x *= scale
self.limited_hinge.pivot_b.y *= scale
self.limited_hinge.pivot_b.z *= scale
def update_a_b(self, parent):
"""Update the B data from the A data."""
transform = self.get_transform_a_b(parent)
self.limited_hinge.update_a_b(transform)
self.ragdoll.update_a_b(transform)
class bhkMoppBvTreeShape:
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center of gravity, and inertia tensor."""
return self.shape.get_mass_center_inertia(density = density, solid = solid)
def update_origin_scale(self):
"""Update scale and origin."""
minx = min(v.x for v in self.shape.data.vertices)
miny = min(v.y for v in self.shape.data.vertices)
minz = min(v.z for v in self.shape.data.vertices)
maxx = max(v.x for v in self.shape.data.vertices)
maxy = max(v.y for v in self.shape.data.vertices)
maxz = max(v.z for v in self.shape.data.vertices)
self.origin.x = minx - 0.1
self.origin.y = miny - 0.1
self.origin.z = minz - 0.1
self.scale = (256*256*254) / (0.2+max([maxx-minx,maxy-miny,maxz-minz]))
def update_mopp(self):
"""Update the MOPP data, scale, and origin, and welding info.
@deprecated: use update_mopp_welding instead
"""
self.update_mopp_welding()
def update_mopp_welding(self):
"""Update the MOPP data, scale, and origin, and welding info."""
logger = logging.getLogger("pyffi.mopp")
# check type of shape
if not isinstance(self.shape, NifFormat.bhkPackedNiTriStripsShape):
raise ValueError(
"expected bhkPackedNiTriStripsShape on mopp"
" but got %s instead" % self.shape.__class__.__name__)
# first try with pyffi.utils.mopp
failed = False
try:
print(pyffi.utils.mopp.getMopperCredits())
except (OSError, RuntimeError):
failed = True
else:
# find material indices per triangle
material_per_vertex = []
subshapes = self.shape.sub_shapes
if not subshapes:
# fallout 3
subshapes = self.shape.data.sub_shapes
for subshape in subshapes:
material_per_vertex += (
[subshape.material] * subshape.num_vertices)
material_per_triangle = [
material_per_vertex[hktri.triangle.v_1]
for hktri in self.shape.data.triangles]
# compute havok info
try:
origin, scale, mopp, welding_infos \
= pyffi.utils.mopp.getMopperOriginScaleCodeWelding(
[vert.as_tuple() for vert in self.shape.data.vertices],
[(hktri.triangle.v_1,
hktri.triangle.v_2,
hktri.triangle.v_3)
for hktri in self.shape.data.triangles],
material_per_triangle)
except (OSError, RuntimeError):
failed = True
else:
# must use calculated scale and origin
self.scale = scale
self.origin.x = origin[0]
self.origin.y = origin[1]
self.origin.z = origin[2]
# if havok's mopper failed, do a simple mopp
if failed:
logger.exception(
"Havok mopp generator failed, falling back on simple mopp "
"(but collisions may be flawed in-game!)."
"If you are using the PyFFI that was shipped with Blender, "
"and you are on Windows, then you may wish to install the "
"full version of PyFFI from "
"http://pyffi.sourceforge.net/ "
"instead, which includes the (closed source) "
"Havok mopp generator.")
self.update_origin_scale()
mopp = self._makeSimpleMopp()
# no welding info
welding_infos = []
# delete mopp and replace with new data
self.mopp_data_size = len(mopp)
self.mopp_data.update_size()
for i, b in enumerate(mopp):
self.mopp_data[i] = b
# update welding information
for hktri, welding_info in izip(self.shape.data.triangles, welding_infos):
hktri.welding_info = welding_info
def _makeSimpleMopp(self):
"""Make a simple mopp."""
mopp = [] # the mopp 'assembly' script
self._q = 256*256 / self.scale # quantization factor
# opcodes
BOUNDX = 0x26
BOUNDY = 0x27
BOUNDZ = 0x28
TESTX = 0x10
TESTY = 0x11
TESTZ = 0x12
# add first crude bounding box checks
self._vertsceil = [ self._moppCeil(v) for v in self.shape.data.vertices ]
self._vertsfloor = [ self._moppFloor(v) for v in self.shape.data.vertices ]
minx = min([ v[0] for v in self._vertsfloor ])
miny = min([ v[1] for v in self._vertsfloor ])
minz = min([ v[2] for v in self._vertsfloor ])
maxx = max([ v[0] for v in self._vertsceil ])
maxy = max([ v[1] for v in self._vertsceil ])
maxz = max([ v[2] for v in self._vertsceil ])
if minx < 0 or miny < 0 or minz < 0: raise ValueError("cannot update mopp tree with invalid origin")
if maxx > 255 or maxy > 255 or maxz > 255: raise ValueError("cannot update mopp tree with invalid scale")
mopp.extend([BOUNDZ, minz, maxz])
mopp.extend([BOUNDY, miny, maxy])
mopp.extend([BOUNDX, minx, maxx])
# add tree using subsequent X-Y-Z splits
# (slow and no noticable difference from other simple tree so deactivated)
#tris = range(len(self.shape.data.triangles))
#tree = self.split_triangles(tris, [[minx,maxx],[miny,maxy],[minz,maxz]])
#mopp += self.mopp_from_tree(tree)
# add a trivial tree
# this prevents the player of walking through the model
# but arrows may still fly through
numtriangles = len(self.shape.data.triangles)
i = 0x30
for t in xrange(numtriangles-1):
mopp.extend([TESTZ, maxz, 0, 1, i])
i += 1
if i == 0x50:
mopp.extend([0x09, 0x20]) # increment triangle offset
i = 0x30
mopp.extend([i])
return mopp
def _moppCeil(self, v):
moppx = int((v.x + 0.1 - self.origin.x) / self._q + 0.99999999)
moppy = int((v.y + 0.1 - self.origin.y) / self._q + 0.99999999)
moppz = int((v.z + 0.1 - self.origin.z) / self._q + 0.99999999)
return [moppx, moppy, moppz]
def _moppFloor(self, v):
moppx = int((v.x - 0.1 - self.origin.x) / self._q)
moppy = int((v.y - 0.1 - self.origin.y) / self._q)
moppz = int((v.z - 0.1 - self.origin.z) / self._q)
return [moppx, moppy, moppz]
def split_triangles(self, ts, bbox, dir=0):
"""Direction 0=X, 1=Y, 2=Z"""
btest = [] # for bounding box tests
test = [] # for branch command
# check bounding box
tris = [ t.triangle for t in self.shape.data.triangles ]
tsverts = [ tris[t].v_1 for t in ts] + [ tris[t].v_2 for t in ts] + [ tris[t].v_3 for t in ts]
minx = min([self._vertsfloor[v][0] for v in tsverts])
miny = min([self._vertsfloor[v][1] for v in tsverts])
minz = min([self._vertsfloor[v][2] for v in tsverts])
maxx = max([self._vertsceil[v][0] for v in tsverts])
maxy = max([self._vertsceil[v][1] for v in tsverts])
maxz = max([self._vertsceil[v][2] for v in tsverts])
# add bounding box checks if it's reduced in a direction
if (maxx - minx < bbox[0][1] - bbox[0][0]):
btest += [ 0x26, minx, maxx ]
bbox[0][0] = minx
bbox[0][1] = maxx
if (maxy - miny < bbox[1][1] - bbox[1][0]):
btest += [ 0x27, miny, maxy ]
bbox[1][0] = miny
bbox[1][1] = maxy
if (maxz - minz < bbox[2][1] - bbox[2][0]):
btest += [ 0x28, minz, maxz ]
bbox[2][0] = minz
bbox[2][1] = maxz
# if only one triangle, no further split needed
if len(ts) == 1:
if ts[0] < 32:
return [ btest, [ 0x30 + ts[0] ], [], [] ]
elif ts[0] < 256:
return [ btest, [ 0x50, ts[0] ], [], [] ]
else:
return [ btest, [ 0x51, ts[0] >> 8, ts[0] & 255 ], [], [] ]
# sort triangles in required direction
ts.sort(key = lambda t: max(self._vertsceil[tris[t].v_1][dir], self._vertsceil[tris[t].v_2][dir], self._vertsceil[tris[t].v_3][dir]))
# split into two
ts1 = ts[:len(ts)/2]
ts2 = ts[len(ts)/2:]
# get maximum coordinate of small group
ts1verts = [ tris[t].v_1 for t in ts1] + [ tris[t].v_2 for t in ts1] + [ tris[t].v_3 for t in ts1]
ts2verts = [ tris[t].v_1 for t in ts2] + [ tris[t].v_2 for t in ts2] + [ tris[t].v_3 for t in ts2]
ts1max = max([self._vertsceil[v][dir] for v in ts1verts])
# get minimum coordinate of large group
ts2min = min([self._vertsfloor[v][dir] for v in ts2verts])
# set up test
test += [0x10+dir, ts1max, ts2min]
# set up new bounding boxes for each subtree
# make copy
bbox1 = [[bbox[0][0],bbox[0][1]],[bbox[1][0],bbox[1][1]],[bbox[2][0],bbox[2][1]]]
bbox2 = [[bbox[0][0],bbox[0][1]],[bbox[1][0],bbox[1][1]],[bbox[2][0],bbox[2][1]]]
# update bound in test direction
bbox1[dir][1] = ts1max
bbox2[dir][0] = ts2min
# return result
nextdir = dir+1
if nextdir == 3: nextdir = 0
return [btest, test, self.split_triangles(ts1, bbox1, nextdir), self.split_triangles(ts2, bbox2, nextdir)]
def mopp_from_tree(self, tree):
if tree[1][0] in xrange(0x30, 0x52):
return tree[0] + tree[1]
mopp = tree[0] + tree[1]
submopp1 = self.mopp_from_tree(tree[2])
submopp2 = self.mopp_from_tree(tree[3])
if len(submopp1) < 256:
mopp += [ len(submopp1) ]
mopp += submopp1
mopp += submopp2
else:
jump = len(submopp2)
if jump <= 255:
mopp += [2, 0x05, jump]
else:
mopp += [3, 0x06, jump >> 8, jump & 255]
mopp += submopp2
mopp += submopp1
return mopp
# ported and extended from NifVis/bhkMoppBvTreeShape.py
def parse_mopp(self, start = 0, depth = 0, toffset = 0, verbose = False):
"""The mopp data is printed to the debug channel
while parsed. Returns list of indices into mopp data of the bytes
processed and a list of triangle indices encountered.
The verbose argument is ignored (and is deprecated).
"""
class Message:
def __init__(self):
self.logger = logging.getLogger("pyffi.mopp")
self.msg = ""
def append(self, *args):
self.msg += " ".join(str(arg) for arg in args) + " "
return self
def debug(self):
if self.msg:
self.logger.debug(self.msg)
self.msg = ""
def error(self):
self.logger.error(self.msg)
self.msg = ""
mopp = self.mopp_data # shortcut notation
ids = [] # indices of bytes processed
tris = [] # triangle indices
i = start # current index
ret = False # set to True if an opcode signals a triangle index
while i < self.mopp_data_size and not ret:
# get opcode and print it
code = mopp[i]
msg = Message()
msg.append("%4i:"%i + " "*depth + '0x%02X ' % code)
if code == 0x09:
# increment triangle offset
toffset += mopp[i+1]
msg.append(mopp[i+1])
msg.append('%i [ triangle offset += %i, offset is now %i ]'
% (mopp[i+1], mopp[i+1], toffset))
ids.extend([i,i+1])
i += 2
elif code in [ 0x0A ]:
# increment triangle offset
toffset += mopp[i+1]*256 + mopp[i+2]
msg.append(mopp[i+1],mopp[i+2])
msg.append('[ triangle offset += %i, offset is now %i ]'
% (mopp[i+1]*256 + mopp[i+2], toffset))
ids.extend([i,i+1,i+2])
i += 3
elif code in [ 0x0B ]:
# unsure about first two arguments, but the 3rd and 4th set triangle offset
toffset = 256*mopp[i+3] + mopp[i+4]
msg.append(mopp[i+1],mopp[i+2],mopp[i+3],mopp[i+4])
msg.append('[ triangle offset = %i ]' % toffset)
ids.extend([i,i+1,i+2,i+3,i+4])
i += 5
elif code in xrange(0x30,0x50):
# triangle compact
msg.append('[ triangle %i ]'%(code-0x30+toffset))
ids.append(i)
tris.append(code-0x30+toffset)
i += 1
ret = True
elif code == 0x50:
# triangle byte
msg.append(mopp[i+1])
msg.append('[ triangle %i ]'%(mopp[i+1]+toffset))
ids.extend([i,i+1])
tris.append(mopp[i+1]+toffset)
i += 2
ret = True
elif code in [ 0x51 ]:
# triangle short
t = mopp[i+1]*256 + mopp[i+2] + toffset
msg.append(mopp[i+1],mopp[i+2])
msg.append('[ triangle %i ]' % t)
ids.extend([i,i+1,i+2])
tris.append(t)
i += 3
ret = True
elif code in [ 0x53 ]:
# triangle short?
t = mopp[i+3]*256 + mopp[i+4] + toffset
msg.append(mopp[i+1],mopp[i+2],mopp[i+3],mopp[i+4])
msg.append('[ triangle %i ]' % t)
ids.extend([i,i+1,i+2,i+3,i+4])
tris.append(t)
i += 5
ret = True
elif code in [ 0x05 ]:
# byte jump
msg.append('[ jump -> %i: ]'%(i+2+mopp[i+1]))
ids.extend([i,i+1])
i += 2+mopp[i+1]
elif code in [ 0x06 ]:
# short jump
jump = mopp[i+1]*256 + mopp[i+2]
msg.append('[ jump -> %i: ]'%(i+3+jump))
ids.extend([i,i+1,i+2])
i += 3+jump
elif code in [0x10,0x11,0x12, 0x13,0x14,0x15, 0x16,0x17,0x18, 0x19, 0x1A, 0x1B, 0x1C]:
# compact if-then-else with two arguments
msg.append(mopp[i+1], mopp[i+2])
if code == 0x10:
msg.append('[ branch X')
elif code == 0x11:
msg.append('[ branch Y')
elif code == 0x12:
msg.append('[ branch Z')
else:
msg.append('[ branch ?')
msg.append('-> %i: %i: ]'%(i+4,i+4+mopp[i+3]))
msg.debug()
msg.append(" " + " "*depth + 'if:')
msg.debug()
idssub1, trissub1 = self.parse_mopp(start = i+4, depth = depth+1, toffset = toffset, verbose = verbose)
msg.append(" " + " "*depth + 'else:')
msg.debug()
idssub2, trissub2 = self.parse_mopp(start = i+4+mopp[i+3], depth = depth+1, toffset = toffset, verbose = verbose)
ids.extend([i,i+1,i+2,i+3])
ids.extend(idssub1)
ids.extend(idssub2)
tris.extend(trissub1)
tris.extend(trissub2)
ret = True
elif code in [0x20,0x21,0x22]:
# compact if-then-else with one argument
msg.append(mopp[i+1], '[ branch ? -> %i: %i: ]'%(i+3,i+3+mopp[i+2])).debug()
msg.append(" " + " "*depth + 'if:').debug()
idssub1, trissub1 = self.parse_mopp(start = i+3, depth = depth+1, toffset = toffset, verbose = verbose)
msg.append(" " + " "*depth + 'else:').debug()
idssub2, trissub2 = self.parse_mopp(start = i+3+mopp[i+2], depth = depth+1, toffset = toffset, verbose = verbose)
ids.extend([i,i+1,i+2])
ids.extend(idssub1)
ids.extend(idssub2)
tris.extend(trissub1)
tris.extend(trissub2)
ret = True
elif code in [0x23,0x24,0x25]: # short if x <= a then 1; if x > b then 2;
jump1 = mopp[i+3] * 256 + mopp[i+4]
jump2 = mopp[i+5] * 256 + mopp[i+6]
msg.append(mopp[i+1], mopp[i+2], '[ branch ? -> %i: %i: ]'%(i+7+jump1,i+7+jump2)).debug()
msg.append(" " + " "*depth + 'if:').debug()
idssub1, trissub1 = self.parse_mopp(start = i+7+jump1, depth = depth+1, toffset = toffset, verbose = verbose)
msg.append(" " + " "*depth + 'else:').debug()
idssub2, trissub2 = self.parse_mopp(start = i+7+jump2, depth = depth+1, toffset = toffset, verbose = verbose)
ids.extend([i,i+1,i+2,i+3,i+4,i+5,i+6])
ids.extend(idssub1)
ids.extend(idssub2)
tris.extend(trissub1)
tris.extend(trissub2)
ret = True
elif code in [0x26,0x27,0x28]:
msg.append(mopp[i+1], mopp[i+2])
if code == 0x26:
msg.append('[ bound X ]')
elif code == 0x27:
msg.append('[ bound Y ]')
elif code == 0x28:
msg.append('[ bound Z ]')
ids.extend([i,i+1,i+2])
i += 3
elif code in [0x01, 0x02, 0x03, 0x04]:
msg.append(mopp[i+1], mopp[i+2], mopp[i+3], '[ bound XYZ? ]')
ids.extend([i,i+1,i+2,i+3])
i += 4
else:
msg.append("unknown mopp code 0x%02X"%code).error()
msg.append("following bytes are").debug()
extrabytes = [mopp[j] for j in xrange(i+1,min(self.mopp_data_size,i+10))]
extraindex = [j for j in xrange(i+1,min(self.mopp_data_size,i+10))]
msg.append(extrabytes).debug()
for b, j in zip(extrabytes, extraindex):
if j+b+1 < self.mopp_data_size:
msg.append("opcode after jump %i is 0x%02X"%(b,mopp[j+b+1]), [mopp[k] for k in xrange(j+b+2,min(self.mopp_data_size,j+b+11))]).debug()
raise ValueError("unknown mopp opcode 0x%02X"%code)
msg.debug()
return ids, tris
class bhkMultiSphereShape:
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return center of gravity and area."""
subshapes_mci = [
(mass, center, inertia)
for (mass, inertia), center in
izip( ( pyffi.utils.inertia.getMassInertiaSphere(radius = sphere.radius,
density = density, solid = solid)
for sphere in self.spheres ),
( sphere.center.as_tuple() for sphere in self.spheres ) ) ]
total_mass = 0
total_center = (0, 0, 0)
total_inertia = ((0, 0, 0), (0, 0, 0), (0, 0, 0))
for mass, center, inertia in subshapes_mci:
total_mass += mass
total_center = vecAdd(total_center,
vecscalarMul(center, mass / total_mass))
total_inertia = matAdd(total_inertia, inertia)
return total_mass, total_center, total_inertia
class bhkNiTriStripsShape:
def get_interchangeable_packed_shape(self):
"""Returns a bhkPackedNiTriStripsShape block that is geometrically
interchangeable.
"""
# get all vertices, triangles, and calculate normals
vertices = []
normals = []
triangles = []
for strip in self.strips_data:
triangles.extend(
(tri1 + len(vertices),
tri2 + len(vertices),
tri3 + len(vertices))
for tri1, tri2, tri3 in strip.get_triangles())
vertices.extend(
# scaling factor 1/7 applied in add_shape later
vert.as_tuple() for vert in strip.vertices)
normals.extend(
(strip.vertices[tri2] - strip.vertices[tri1]).crossproduct(
strip.vertices[tri3] - strip.vertices[tri1])
.normalized()
.as_tuple()
for tri1, tri2, tri3 in strip.get_triangles())
# create packed shape and add geometry
packed = NifFormat.bhkPackedNiTriStripsShape()
packed.add_shape(
triangles=triangles,
normals=normals,
vertices=vertices,
# default layer 1 (static collision)
layer=self.data_layers[0].layer if self.data_layers else 1,
material=self.material)
# set unknowns
packed.unknown_floats[2] = 0.1
packed.unknown_floats[4] = 1.0
packed.unknown_floats[5] = 1.0
packed.unknown_floats[6] = 1.0
packed.unknown_floats[8] = 0.1
packed.scale = 1.0
packed.unknown_floats_2[0] = 1.0
packed.unknown_floats_2[1] = 1.0
# return result
return packed
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center, and inertia tensor."""
# first find mass, center, and inertia of all shapes
subshapes_mci = []
for data in self.strips_data:
subshapes_mci.append(
pyffi.utils.inertia.get_mass_center_inertia_polyhedron(
[ vert.as_tuple() for vert in data.vertices ],
[ triangle for triangle in data.get_triangles() ],
density = density, solid = solid))
# now calculate mass, center, and inertia
total_mass = 0
total_center = (0, 0, 0)
total_inertia = ((0, 0, 0), (0, 0, 0), (0, 0, 0))
for mass, center, inertia in subshapes_mci:
total_mass += mass
total_center = vecAdd(total_center,
vecscalarMul(center, mass / total_mass))
total_inertia = matAdd(total_inertia, inertia)
return total_mass, total_center, total_inertia
class bhkPackedNiTriStripsShape:
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center, and inertia tensor."""
return pyffi.utils.inertia.get_mass_center_inertia_polyhedron(
[ vert.as_tuple() for vert in self.data.vertices ],
[ ( hktriangle.triangle.v_1,
hktriangle.triangle.v_2,
hktriangle.triangle.v_3 )
for hktriangle in self.data.triangles ],
density = density, solid = solid)
def add_shape(self, triangles, normals, vertices, layer = 0, material = 0):
"""Pack the given geometry."""
# add the shape data
if not self.data:
self.data = NifFormat.hkPackedNiTriStripsData()
data = self.data
# increase number of shapes
num_shapes = self.num_sub_shapes
self.num_sub_shapes = num_shapes + 1
self.sub_shapes.update_size()
data.num_sub_shapes = num_shapes + 1
data.sub_shapes.update_size()
# add the shape
self.sub_shapes[num_shapes].layer = layer
self.sub_shapes[num_shapes].num_vertices = len(vertices)
self.sub_shapes[num_shapes].material = material
data.sub_shapes[num_shapes].layer = layer
data.sub_shapes[num_shapes].num_vertices = len(vertices)
data.sub_shapes[num_shapes].material = material
firsttriangle = data.num_triangles
firstvertex = data.num_vertices
data.num_triangles += len(triangles)
data.triangles.update_size()
for tdata, t, n in zip(data.triangles[firsttriangle:], triangles, normals):
tdata.triangle.v_1 = t[0] + firstvertex
tdata.triangle.v_2 = t[1] + firstvertex
tdata.triangle.v_3 = t[2] + firstvertex
tdata.normal.x = n[0]
tdata.normal.y = n[1]
tdata.normal.z = n[2]
data.num_vertices += len(vertices)
data.vertices.update_size()
for vdata, v in zip(data.vertices[firstvertex:], vertices):
vdata.x = v[0] / 7.0
vdata.y = v[1] / 7.0
vdata.z = v[2] / 7.0
class bhkRagdollConstraint:
def apply_scale(self, scale):
"""Scale data."""
# apply scale on transform
self.ragdoll.pivot_a.x *= scale
self.ragdoll.pivot_a.y *= scale
self.ragdoll.pivot_a.z *= scale
self.ragdoll.pivot_b.x *= scale
self.ragdoll.pivot_b.y *= scale
self.ragdoll.pivot_b.z *= scale
def update_a_b(self, parent):
"""Update the B data from the A data."""
self.ragdoll.update_a_b(self.get_transform_a_b(parent))
class bhkRigidBody:
def apply_scale(self, scale):
"""Apply scale factor <scale> on data."""
# apply scale on transform
self.translation.x *= scale
self.translation.y *= scale
self.translation.z *= scale
# apply scale on center of gravity
self.center.x *= scale
self.center.y *= scale
self.center.z *= scale
# apply scale on inertia tensor
self.inertia.m_11 *= (scale ** 2)
self.inertia.m_12 *= (scale ** 2)
self.inertia.m_13 *= (scale ** 2)
self.inertia.m_14 *= (scale ** 2)
self.inertia.m_21 *= (scale ** 2)
self.inertia.m_22 *= (scale ** 2)
self.inertia.m_23 *= (scale ** 2)
self.inertia.m_24 *= (scale ** 2)
self.inertia.m_31 *= (scale ** 2)
self.inertia.m_32 *= (scale ** 2)
self.inertia.m_33 *= (scale ** 2)
self.inertia.m_34 *= (scale ** 2)
def update_mass_center_inertia(self, density = 1, solid = True, mass = None):
"""Look at all the objects under this rigid body and update the mass,
center of gravity, and inertia tensor accordingly. If the C{mass} parameter
is given then the C{density} argument is ignored."""
if not mass is None:
density = 1
calc_mass, center, inertia = self.shape.get_mass_center_inertia(
density = density, solid = solid)
self.mass = calc_mass
self.center.x, self.center.y, self.center.z = center
self.inertia.m_11 = inertia[0][0]
self.inertia.m_12 = inertia[0][1]
self.inertia.m_13 = inertia[0][2]
self.inertia.m_14 = 0
self.inertia.m_21 = inertia[1][0]
self.inertia.m_22 = inertia[1][1]
self.inertia.m_23 = inertia[1][2]
self.inertia.m_24 = 0
self.inertia.m_31 = inertia[2][0]
self.inertia.m_32 = inertia[2][1]
self.inertia.m_33 = inertia[2][2]
self.inertia.m_34 = 0
if not mass is None:
mass_correction = mass / calc_mass if calc_mass != 0 else 1
self.mass = mass
self.inertia.m_11 *= mass_correction
self.inertia.m_12 *= mass_correction
self.inertia.m_13 *= mass_correction
self.inertia.m_14 *= mass_correction
self.inertia.m_21 *= mass_correction
self.inertia.m_22 *= mass_correction
self.inertia.m_23 *= mass_correction
self.inertia.m_24 *= mass_correction
self.inertia.m_31 *= mass_correction
self.inertia.m_32 *= mass_correction
self.inertia.m_33 *= mass_correction
self.inertia.m_34 *= mass_correction
class bhkSphereShape:
def apply_scale(self, scale):
"""Apply scale factor <scale> on data."""
# apply scale on dimensions
self.radius *= scale
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center, and inertia tensor."""
# the dimensions describe half the size of the box in each dimension
# so the length of a single edge is dimension.dir * 2
mass, inertia = pyffi.utils.inertia.getMassInertiaSphere(
self.radius, density = density, solid = solid)
return mass, (0,0,0), inertia
class bhkTransformShape:
def apply_scale(self, scale):
"""Apply scale factor <scale> on data."""
# apply scale on translation
self.transform.m_14 *= scale
self.transform.m_24 *= scale
self.transform.m_34 *= scale
def get_mass_center_inertia(self, density = 1, solid = True):
"""Return mass, center, and inertia tensor."""
# get shape mass, center, and inertia
mass, center, inertia = self.shape.get_mass_center_inertia(density = density,
solid = solid)
# get transform matrix and translation vector
transform = self.transform.get_matrix_33().as_tuple()
transform_transposed = matTransposed(transform)
translation = ( self.transform.m_14, self.transform.m_24, self.transform.m_34 )
# transform center and inertia
center = matvecMul(transform, center)
center = vecAdd(center, translation)
inertia = matMul(matMul(transform_transposed, inertia), transform)
# return updated mass center and inertia
return mass, center, inertia
class BSBound:
def apply_scale(self, scale):
"""Scale data."""
self.center.x *= scale
self.center.y *= scale
self.center.z *= scale
self.dimensions.x *= scale
self.dimensions.y *= scale
self.dimensions.z *= scale
class ControllerLink:
"""
>>> from pyffi.formats.nif import NifFormat
>>> link = NifFormat.ControllerLink()
>>> link.node_name_offset
-1
>>> link.set_node_name("Bip01")
>>> link.node_name_offset
0
>>> link.get_node_name()
'Bip01'
>>> link.node_name
'Bip01'
>>> link.set_node_name("Bip01 Tail")
>>> link.node_name_offset
6
>>> link.get_node_name()
'Bip01 Tail'
>>> link.node_name
'Bip01 Tail'
"""
def _get_string(self, offset):
"""A wrapper around string_palette.palette.get_string. Used by get_node_name
etc. Returns the string at given offset."""
if offset == -1:
return ''
if not self.string_palette:
return ''
return self.string_palette.palette.get_string(offset)
def _add_string(self, text):
"""Wrapper for string_palette.palette.add_string. Used by set_node_name etc.
Returns offset of string added."""
# create string palette if none exists yet
if not self.string_palette:
self.string_palette = NifFormat.NiStringPalette()
# add the string and return the offset
return self.string_palette.palette.add_string(text)
def get_node_name(self):
"""Return the node name.
>>> # a doctest
>>> from pyffi.formats.nif import NifFormat
>>> link = NifFormat.ControllerLink()
>>> link.string_palette = NifFormat.NiStringPalette()
>>> palette = link.string_palette.palette
>>> link.node_name_offset = palette.add_string("Bip01")
>>> link.get_node_name()
'Bip01'
>>> # another doctest
>>> from pyffi.formats.nif import NifFormat
>>> link = NifFormat.ControllerLink()
>>> link.node_name = "Bip01"
>>> link.get_node_name()
'Bip01'
"""
if self.node_name:
return self.node_name
else:
return self._get_string(self.node_name_offset)
def set_node_name(self, text):
self.node_name = text
self.node_name_offset = self._add_string(text)
def get_property_type(self):
if self.property_type:
return self.property_type
else:
return self._get_string(self.property_type_offset)
def set_property_type(self, text):
self.property_type = text
self.property_type_offset = self._add_string(text)
def get_controller_type(self):
if self.controller_type:
return self.controller_type
else:
return self._get_string(self.controller_type_offset)
def set_controller_type(self, text):
self.controller_type = text
self.controller_type_offset = self._add_string(text)
def get_variable_1(self):
if self.variable_1:
return self.variable_1
else:
return self._get_string(self.variable_1_offset)
def set_variable_1(self, text):
self.variable_1 = text
self.variable_1_offset = self._add_string(text)
def get_variable_2(self):
if self.variable_2:
return self.variable_2
else:
return self._get_string(self.variable_2_offset)
def set_variable_2(self, text):
self.variable_2 = text
self.variable_2_offset = self._add_string(text)
class hkPackedNiTriStripsData:
def apply_scale(self, scale):
"""Apply scale factor on data."""
if abs(scale - 1.0) < NifFormat.EPSILON:
return
for vert in self.vertices:
vert.x *= scale
vert.y *= scale
vert.z *= scale
class InertiaMatrix:
def as_list(self):
"""Return matrix as 3x3 list."""
return [
[self.m_11, self.m_12, self.m_13],
[self.m_21, self.m_22, self.m_23],
[self.m_31, self.m_32, self.m_33]
]
def as_tuple(self):
"""Return matrix as 3x3 tuple."""
return (
(self.m_11, self.m_12, self.m_13),
(self.m_21, self.m_22, self.m_23),
(self.m_31, self.m_32, self.m_33)
)
def __str__(self):
return(
"[ %6.3f %6.3f %6.3f ]\n"
"[ %6.3f %6.3f %6.3f ]\n"
"[ %6.3f %6.3f %6.3f ]\n"
% (self.m_11, self.m_12, self.m_13,
self.m_21, self.m_22, self.m_23,
self.m_31, self.m_32, self.m_33))
def set_identity(self):
"""Set to identity matrix."""
self.m_11 = 1.0
self.m_12 = 0.0
self.m_13 = 0.0
self.m_14 = 0.0
self.m_21 = 0.0
self.m_22 = 1.0
self.m_23 = 0.0
self.m_24 = 0.0
self.m_31 = 0.0
self.m_32 = 0.0
self.m_33 = 1.0
self.m_34 = 0.0
def is_identity(self):
"""Return ``True`` if the matrix is close to identity."""
if (abs(self.m_11 - 1.0) > NifFormat.EPSILON
or abs(self.m_12) > NifFormat.EPSILON
or abs(self.m_13) > NifFormat.EPSILON
or abs(self.m_21) > NifFormat.EPSILON
or abs(self.m_22 - 1.0) > NifFormat.EPSILON
or abs(self.m_23) > NifFormat.EPSILON
or abs(self.m_31) > NifFormat.EPSILON
or abs(self.m_32) > NifFormat.EPSILON
or abs(self.m_33 - 1.0) > NifFormat.EPSILON):
return False
else:
return True
def get_copy(self):
"""Return a copy of the matrix."""
mat = NifFormat.InertiaMatrix()
mat.m_11 = self.m_11
mat.m_12 = self.m_12
mat.m_13 = self.m_13
mat.m_14 = self.m_14
mat.m_21 = self.m_21
mat.m_22 = self.m_22
mat.m_23 = self.m_23
mat.m_24 = self.m_24
mat.m_31 = self.m_31
mat.m_32 = self.m_32
mat.m_33 = self.m_33
mat.m_34 = self.m_34
return mat
def __eq__(self, mat):
if not isinstance(mat, NifFormat.InertiaMatrix):
raise TypeError(
"do not know how to compare InertiaMatrix and %s"%mat.__class__)
if (abs(self.m_11 - mat.m_11) > NifFormat.EPSILON
or abs(self.m_12 - mat.m_12) > NifFormat.EPSILON
or abs(self.m_13 - mat.m_13) > NifFormat.EPSILON
or abs(self.m_21 - mat.m_21) > NifFormat.EPSILON
or abs(self.m_22 - mat.m_22) > NifFormat.EPSILON
or abs(self.m_23 - mat.m_23) > NifFormat.EPSILON
or abs(self.m_31 - mat.m_31) > NifFormat.EPSILON
or abs(self.m_32 - mat.m_32) > NifFormat.EPSILON
or abs(self.m_33 - mat.m_33) > NifFormat.EPSILON):
return False
return True
def __ne__(self, mat):
return not self.__eq__(mat)
class LimitedHingeDescriptor:
def update_a_b(self, transform):
"""Update B pivot and axes from A using the given transform."""
# pivot point
pivot_b = ((7 * self.pivot_a.get_vector_3()) * transform) / 7.0
self.pivot_b.x = pivot_b.x
self.pivot_b.y = pivot_b.y
self.pivot_b.z = pivot_b.z
# axes (rotation only)
transform = transform.get_matrix_33()
axle_b = self.axle_a.get_vector_3() * transform
perp_2_axle_in_b_2 = self.perp_2_axle_in_a_2.get_vector_3() * transform
self.axle_b.x = axle_b.x
self.axle_b.y = axle_b.y
self.axle_b.z = axle_b.z
self.perp_2_axle_in_b_2.x = perp_2_axle_in_b_2.x
self.perp_2_axle_in_b_2.y = perp_2_axle_in_b_2.y
self.perp_2_axle_in_b_2.z = perp_2_axle_in_b_2.z
class Matrix44:
def as_list(self):
"""Return matrix as 4x4 list."""
return [
[self.m_11, self.m_12, self.m_13, self.m_14],
[self.m_21, self.m_22, self.m_23, self.m_24],
[self.m_31, self.m_32, self.m_33, self.m_34],
[self.m_41, self.m_42, self.m_43, self.m_44]
]
def as_tuple(self):
"""Return matrix as 4x4 tuple."""
return (
(self.m_11, self.m_12, self.m_13, self.m_14),
(self.m_21, self.m_22, self.m_23, self.m_24),
(self.m_31, self.m_32, self.m_33, self.m_34),
(self.m_41, self.m_42, self.m_43, self.m_44)
)
def set_rows(self, row0, row1, row2, row3):
"""Set matrix from rows."""
self.m_11, self.m_12, self.m_13, self.m_14 = row0
self.m_21, self.m_22, self.m_23, self.m_24 = row1
self.m_31, self.m_32, self.m_33, self.m_34 = row2
self.m_41, self.m_42, self.m_43, self.m_44 = row3
def __str__(self):
return(
"[ %6.3f %6.3f %6.3f %6.3f ]\n"
"[ %6.3f %6.3f %6.3f %6.3f ]\n"
"[ %6.3f %6.3f %6.3f %6.3f ]\n"
"[ %6.3f %6.3f %6.3f %6.3f ]\n"
% (self.m_11, self.m_12, self.m_13, self.m_14,
self.m_21, self.m_22, self.m_23, self.m_24,
self.m_31, self.m_32, self.m_33, self.m_34,
self.m_41, self.m_42, self.m_43, self.m_44))
def set_identity(self):
"""Set to identity matrix."""
self.m_11 = 1.0
self.m_12 = 0.0
self.m_13 = 0.0
self.m_14 = 0.0
self.m_21 = 0.0
self.m_22 = 1.0
self.m_23 = 0.0
self.m_24 = 0.0
self.m_31 = 0.0
self.m_32 = 0.0
self.m_33 = 1.0
self.m_34 = 0.0
self.m_41 = 0.0
self.m_42 = 0.0
self.m_43 = 0.0
self.m_44 = 1.0
def is_identity(self):
"""Return ``True`` if the matrix is close to identity."""
if (abs(self.m_11 - 1.0) > NifFormat.EPSILON
or abs(self.m_12) > NifFormat.EPSILON
or abs(self.m_13) > NifFormat.EPSILON
or abs(self.m_14) > NifFormat.EPSILON
or abs(self.m_21) > NifFormat.EPSILON
or abs(self.m_22 - 1.0) > NifFormat.EPSILON
or abs(self.m_23) > NifFormat.EPSILON
or abs(self.m_24) > NifFormat.EPSILON
or abs(self.m_31) > NifFormat.EPSILON
or abs(self.m_32) > NifFormat.EPSILON
or abs(self.m_33 - 1.0) > NifFormat.EPSILON
or abs(self.m_34) > NifFormat.EPSILON
or abs(self.m_41) > NifFormat.EPSILON
or abs(self.m_42) > NifFormat.EPSILON
or abs(self.m_43) > NifFormat.EPSILON
or abs(self.m_44 - 1.0) > NifFormat.EPSILON):
return False
else:
return True
def get_copy(self):
"""Create a copy of the matrix."""
mat = NifFormat.Matrix44()
mat.m_11 = self.m_11
mat.m_12 = self.m_12
mat.m_13 = self.m_13
mat.m_14 = self.m_14
mat.m_21 = self.m_21
mat.m_22 = self.m_22
mat.m_23 = self.m_23
mat.m_24 = self.m_24
mat.m_31 = self.m_31
mat.m_32 = self.m_32
mat.m_33 = self.m_33
mat.m_34 = self.m_34
mat.m_41 = self.m_41
mat.m_42 = self.m_42
mat.m_43 = self.m_43
mat.m_44 = self.m_44
return mat
def get_matrix_33(self):
"""Returns upper left 3x3 part."""
m = NifFormat.Matrix33()
m.m_11 = self.m_11
m.m_12 = self.m_12
m.m_13 = self.m_13
m.m_21 = self.m_21
m.m_22 = self.m_22
m.m_23 = self.m_23
m.m_31 = self.m_31
m.m_32 = self.m_32
m.m_33 = self.m_33
return m
def set_matrix_33(self, m):
"""Sets upper left 3x3 part."""
if not isinstance(m, NifFormat.Matrix33):
raise TypeError('argument must be Matrix33')
self.m_11 = m.m_11
self.m_12 = m.m_12
self.m_13 = m.m_13
self.m_21 = m.m_21
self.m_22 = m.m_22
self.m_23 = m.m_23
self.m_31 = m.m_31
self.m_32 = m.m_32
self.m_33 = m.m_33
def get_translation(self):
"""Returns lower left 1x3 part."""
t = NifFormat.Vector3()
t.x = self.m_41
t.y = self.m_42
t.z = self.m_43
return t
def set_translation(self, translation):
"""Returns lower left 1x3 part."""
if not isinstance(translation, NifFormat.Vector3):
raise TypeError('argument must be Vector3')
self.m_41 = translation.x
self.m_42 = translation.y
self.m_43 = translation.z
def is_scale_rotation_translation(self):
if not self.get_matrix_33().is_scale_rotation(): return False
if abs(self.m_14) > NifFormat.EPSILON: return False
if abs(self.m_24) > NifFormat.EPSILON: return False
if abs(self.m_34) > NifFormat.EPSILON: return False
if abs(self.m_44 - 1.0) > NifFormat.EPSILON: return False
return True
def get_scale_rotation_translation(self):
rotscl = self.get_matrix_33()
scale = rotscl.get_scale()
rot = rotscl / scale
trans = self.get_translation()
return (scale, rot, trans)
def get_scale_quat_translation(self):
rotscl = self.get_matrix_33()
scale, quat = rotscl.get_scale_quat()
trans = self.get_translation()
return (scale, quat, trans)
def set_scale_rotation_translation(self, scale, rotation, translation):
if not isinstance(scale, (float, int, long)):
raise TypeError('scale must be float')
if not isinstance(rotation, NifFormat.Matrix33):
raise TypeError('rotation must be Matrix33')
if not isinstance(translation, NifFormat.Vector3):
raise TypeError('translation must be Vector3')
if not rotation.is_rotation():
logger = logging.getLogger("pyffi.nif.matrix")
mat = rotation * rotation.get_transpose()
idmat = NifFormat.Matrix33()
idmat.set_identity()
error = (mat - idmat).sup_norm()
logger.warning("improper rotation matrix (error is %f)" % error)
logger.debug(" matrix =")
for line in str(rotation).split("\n"):
logger.debug(" %s" % line)
logger.debug(" its determinant = %f" % rotation.get_determinant())
logger.debug(" matrix * matrix^T =")
for line in str(mat).split("\n"):
logger.debug(" %s" % line)
self.m_14 = 0.0
self.m_24 = 0.0
self.m_34 = 0.0
self.m_44 = 1.0
self.set_matrix_33(rotation * scale)
self.set_translation(translation)
def get_inverse(self, fast=True):
"""Calculates inverse (fast assumes is_scale_rotation_translation is True)."""
def adjoint(m, ii, jj):
result = []
for i, row in enumerate(m):
if i == ii: continue
result.append([])
for j, x in enumerate(row):
if j == jj: continue
result[-1].append(x)
return result
def determinant(m):
if len(m) == 2:
return m[0][0]*m[1][1] - m[1][0]*m[0][1]
result = 0.0
for i in xrange(len(m)):
det = determinant(adjoint(m, i, 0))
if i & 1:
result -= m[i][0] * det
else:
result += m[i][0] * det
return result
if fast:
m = self.get_matrix_33().get_inverse()
t = -(self.get_translation() * m)
n = NifFormat.Matrix44()
n.m_14 = 0.0
n.m_24 = 0.0
n.m_34 = 0.0
n.m_44 = 1.0
n.set_matrix_33(m)
n.set_translation(t)
return n
else:
m = self.as_list()
nn = [[0.0 for i in xrange(4)] for j in xrange(4)]
det = determinant(m)
if abs(det) < NifFormat.EPSILON:
raise ZeroDivisionError('cannot invert matrix:\n%s'%self)
for i in xrange(4):
for j in xrange(4):
if (i+j) & 1:
nn[j][i] = -determinant(adjoint(m, i, j)) / det
else:
nn[j][i] = determinant(adjoint(m, i, j)) / det
n = NifFormat.Matrix44()
n.set_rows(*nn)
return n
def __mul__(self, x):
if isinstance(x, (float, int, long)):
m = NifFormat.Matrix44()
m.m_11 = self.m_11 * x
m.m_12 = self.m_12 * x
m.m_13 = self.m_13 * x
m.m_14 = self.m_14 * x
m.m_21 = self.m_21 * x
m.m_22 = self.m_22 * x
m.m_23 = self.m_23 * x
m.m_24 = self.m_24 * x
m.m_31 = self.m_31 * x
m.m_32 = self.m_32 * x
m.m_33 = self.m_33 * x
m.m_34 = self.m_34 * x
m.m_41 = self.m_41 * x
m.m_42 = self.m_42 * x
m.m_43 = self.m_43 * x
m.m_44 = self.m_44 * x
return m
elif isinstance(x, NifFormat.Vector3):
raise TypeError("matrix*vector not supported; please use left multiplication (vector*matrix)")
elif isinstance(x, NifFormat.Vector4):
raise TypeError("matrix*vector not supported; please use left multiplication (vector*matrix)")
elif isinstance(x, NifFormat.Matrix44):
m = NifFormat.Matrix44()
m.m_11 = self.m_11 * x.m_11 + self.m_12 * x.m_21 + self.m_13 * x.m_31 + self.m_14 * x.m_41
m.m_12 = self.m_11 * x.m_12 + self.m_12 * x.m_22 + self.m_13 * x.m_32 + self.m_14 * x.m_42
m.m_13 = self.m_11 * x.m_13 + self.m_12 * x.m_23 + self.m_13 * x.m_33 + self.m_14 * x.m_43
m.m_14 = self.m_11 * x.m_14 + self.m_12 * x.m_24 + self.m_13 * x.m_34 + self.m_14 * x.m_44
m.m_21 = self.m_21 * x.m_11 + self.m_22 * x.m_21 + self.m_23 * x.m_31 + self.m_24 * x.m_41
m.m_22 = self.m_21 * x.m_12 + self.m_22 * x.m_22 + self.m_23 * x.m_32 + self.m_24 * x.m_42
m.m_23 = self.m_21 * x.m_13 + self.m_22 * x.m_23 + self.m_23 * x.m_33 + self.m_24 * x.m_43
m.m_24 = self.m_21 * x.m_14 + self.m_22 * x.m_24 + self.m_23 * x.m_34 + self.m_24 * x.m_44
m.m_31 = self.m_31 * x.m_11 + self.m_32 * x.m_21 + self.m_33 * x.m_31 + self.m_34 * x.m_41
m.m_32 = self.m_31 * x.m_12 + self.m_32 * x.m_22 + self.m_33 * x.m_32 + self.m_34 * x.m_42
m.m_33 = self.m_31 * x.m_13 + self.m_32 * x.m_23 + self.m_33 * x.m_33 + self.m_34 * x.m_43
m.m_34 = self.m_31 * x.m_14 + self.m_32 * x.m_24 + self.m_33 * x.m_34 + self.m_34 * x.m_44
m.m_41 = self.m_41 * x.m_11 + self.m_42 * x.m_21 + self.m_43 * x.m_31 + self.m_44 * x.m_41
m.m_42 = self.m_41 * x.m_12 + self.m_42 * x.m_22 + self.m_43 * x.m_32 + self.m_44 * x.m_42
m.m_43 = self.m_41 * x.m_13 + self.m_42 * x.m_23 + self.m_43 * x.m_33 + self.m_44 * x.m_43
m.m_44 = self.m_41 * x.m_14 + self.m_42 * x.m_24 + self.m_43 * x.m_34 + self.m_44 * x.m_44
return m
else:
raise TypeError("do not know how to multiply Matrix44 with %s"%x.__class__)
def __div__(self, x):
if isinstance(x, (float, int, long)):
m = NifFormat.Matrix44()
m.m_11 = self.m_11 / x
m.m_12 = self.m_12 / x
m.m_13 = self.m_13 / x
m.m_14 = self.m_14 / x
m.m_21 = self.m_21 / x
m.m_22 = self.m_22 / x
m.m_23 = self.m_23 / x
m.m_24 = self.m_24 / x
m.m_31 = self.m_31 / x
m.m_32 = self.m_32 / x
m.m_33 = self.m_33 / x
m.m_34 = self.m_34 / x
m.m_41 = self.m_41 / x
m.m_42 = self.m_42 / x
m.m_43 = self.m_43 / x
m.m_44 = self.m_44 / x
return m
else:
raise TypeError("do not know how to divide Matrix44 by %s"%x.__class__)
# py3k
__truediv__ = __div__
def __rmul__(self, x):
if isinstance(x, (float, int, long)):
return self * x
else:
raise TypeError("do not know how to multiply %s with Matrix44"%x.__class__)
def __eq__(self, m):
if isinstance(m, type(None)):
return False
if not isinstance(m, NifFormat.Matrix44):
raise TypeError("do not know how to compare Matrix44 and %s"%m.__class__)
if abs(self.m_11 - m.m_11) > NifFormat.EPSILON: return False
if abs(self.m_12 - m.m_12) > NifFormat.EPSILON: return False
if abs(self.m_13 - m.m_13) > NifFormat.EPSILON: return False
if abs(self.m_14 - m.m_14) > NifFormat.EPSILON: return False
if abs(self.m_21 - m.m_21) > NifFormat.EPSILON: return False
if abs(self.m_22 - m.m_22) > NifFormat.EPSILON: return False
if abs(self.m_23 - m.m_23) > NifFormat.EPSILON: return False
if abs(self.m_24 - m.m_24) > NifFormat.EPSILON: return False
if abs(self.m_31 - m.m_31) > NifFormat.EPSILON: return False
if abs(self.m_32 - m.m_32) > NifFormat.EPSILON: return False
if abs(self.m_33 - m.m_33) > NifFormat.EPSILON: return False
if abs(self.m_34 - m.m_34) > NifFormat.EPSILON: return False
if abs(self.m_41 - m.m_41) > NifFormat.EPSILON: return False
if abs(self.m_42 - m.m_42) > NifFormat.EPSILON: return False
if abs(self.m_43 - m.m_43) > NifFormat.EPSILON: return False
if abs(self.m_44 - m.m_44) > NifFormat.EPSILON: return False
return True
def __ne__(self, m):
return not self.__eq__(m)
def __add__(self, x):
if isinstance(x, (NifFormat.Matrix44)):
m = NifFormat.Matrix44()
m.m_11 = self.m_11 + x.m_11
m.m_12 = self.m_12 + x.m_12
m.m_13 = self.m_13 + x.m_13
m.m_14 = self.m_14 + x.m_14
m.m_21 = self.m_21 + x.m_21
m.m_22 = self.m_22 + x.m_22
m.m_23 = self.m_23 + x.m_23
m.m_24 = self.m_24 + x.m_24
m.m_31 = self.m_31 + x.m_31
m.m_32 = self.m_32 + x.m_32
m.m_33 = self.m_33 + x.m_33
m.m_34 = self.m_34 + x.m_34
m.m_41 = self.m_41 + x.m_41
m.m_42 = self.m_42 + x.m_42
m.m_43 = self.m_43 + x.m_43
m.m_44 = self.m_44 + x.m_44
return m
elif isinstance(x, (int, long, float)):
m = NifFormat.Matrix44()
m.m_11 = self.m_11 + x
m.m_12 = self.m_12 + x
m.m_13 = self.m_13 + x
m.m_14 = self.m_14 + x
m.m_21 = self.m_21 + x
m.m_22 = self.m_22 + x
m.m_23 = self.m_23 + x
m.m_24 = self.m_24 + x
m.m_31 = self.m_31 + x
m.m_32 = self.m_32 + x
m.m_33 = self.m_33 + x
m.m_34 = self.m_34 + x
m.m_41 = self.m_41 + x
m.m_42 = self.m_42 + x
m.m_43 = self.m_43 + x
m.m_44 = self.m_44 + x
return m
else:
raise TypeError("do not know how to add Matrix44 and %s"%x.__class__)
def __sub__(self, x):
if isinstance(x, (NifFormat.Matrix44)):
m = NifFormat.Matrix44()
m.m_11 = self.m_11 - x.m_11
m.m_12 = self.m_12 - x.m_12
m.m_13 = self.m_13 - x.m_13
m.m_14 = self.m_14 - x.m_14
m.m_21 = self.m_21 - x.m_21
m.m_22 = self.m_22 - x.m_22
m.m_23 = self.m_23 - x.m_23
m.m_24 = self.m_24 - x.m_24
m.m_31 = self.m_31 - x.m_31
m.m_32 = self.m_32 - x.m_32
m.m_33 = self.m_33 - x.m_33
m.m_34 = self.m_34 - x.m_34
m.m_41 = self.m_41 - x.m_41
m.m_42 = self.m_42 - x.m_42
m.m_43 = self.m_43 - x.m_43
m.m_44 = self.m_44 - x.m_44
return m
elif isinstance(x, (int, long, float)):
m = NifFormat.Matrix44()
m.m_11 = self.m_11 - x
m.m_12 = self.m_12 - x
m.m_13 = self.m_13 - x
m.m_14 = self.m_14 - x
m.m_21 = self.m_21 - x
m.m_22 = self.m_22 - x
m.m_23 = self.m_23 - x
m.m_24 = self.m_24 - x
m.m_31 = self.m_31 - x
m.m_32 = self.m_32 - x
m.m_33 = self.m_33 - x
m.m_34 = self.m_34 - x
m.m_41 = self.m_41 - x
m.m_42 = self.m_42 - x
m.m_43 = self.m_43 - x
m.m_44 = self.m_44 - x
return m
else:
raise TypeError("do not know how to substract Matrix44 and %s"
% x.__class__)
def sup_norm(self):
"""Calculate supremum norm of matrix (maximum absolute value of all
entries)."""
return max(max(abs(elem) for elem in row)
for row in self.as_list())
class NiAVObject:
"""
>>> from pyffi.formats.nif import NifFormat
>>> node = NifFormat.NiNode()
>>> prop1 = NifFormat.NiProperty()
>>> prop1.name = "hello"
>>> prop2 = NifFormat.NiProperty()
>>> prop2.name = "world"
>>> node.get_properties()
[]
>>> node.set_properties([prop1, prop2])
>>> [prop.name for prop in node.get_properties()]
['hello', 'world']
>>> [prop.name for prop in node.properties]
['hello', 'world']
>>> node.set_properties([])
>>> node.get_properties()
[]
>>> # now set them the other way around
>>> node.set_properties([prop2, prop1])
>>> [prop.name for prop in node.get_properties()]
['world', 'hello']
>>> [prop.name for prop in node.properties]
['world', 'hello']
>>> node.remove_property(prop2)
>>> [prop.name for prop in node.properties]
['hello']
>>> node.add_property(prop2)
>>> [prop.name for prop in node.properties]
['hello', 'world']
"""
def add_property(self, prop):
"""Add the given property to the property list.
:param prop: The property block to add.
:type prop: L{NifFormat.NiProperty}
"""
num_props = self.num_properties
self.num_properties = num_props + 1
self.properties.update_size()
self.properties[num_props] = prop
def remove_property(self, prop):
"""Remove the given property to the property list.
:param prop: The property block to remove.
:type prop: L{NifFormat.NiProperty}
"""
self.set_properties([otherprop for otherprop in self.get_properties()
if not(otherprop is prop)])
def get_properties(self):
"""Return a list of the properties of the block.
:return: The list of properties.
:rtype: ``list`` of L{NifFormat.NiProperty}
"""
return [prop for prop in self.properties]
def set_properties(self, proplist):
"""Set the list of properties from the given list (destroys existing list).
:param proplist: The list of property blocks to set.
:type proplist: ``list`` of L{NifFormat.NiProperty}
"""
self.num_properties = len(proplist)
self.properties.update_size()
for i, prop in enumerate(proplist):
self.properties[i] = prop
def get_transform(self, relative_to=None):
"""Return scale, rotation, and translation into a single 4x4
matrix, relative to the C{relative_to} block (which should be
another NiAVObject connecting to this block). If C{relative_to} is
``None``, then returns the transform stored in C{self}, or
equivalently, the target is assumed to be the parent.
:param relative_to: The block relative to which the transform must
be calculated. If ``None``, the local transform is returned.
"""
m = NifFormat.Matrix44()
m.set_scale_rotation_translation(self.scale, self.rotation, self.translation)
if not relative_to: return m
# find chain from relative_to to self
chain = relative_to.find_chain(self, block_type = NifFormat.NiAVObject)
if not chain:
raise ValueError(
'cannot find a chain of NiAVObject blocks '
'between %s and %s.' % (self.name, relative_to.name))
# and multiply with all transform matrices (not including relative_to)
for block in reversed(chain[1:-1]):
m *= block.get_transform()
return m
def set_transform(self, m):
"""Set rotation, translation, and scale, from a 4x4 matrix.
:param m: The matrix to which the transform should be set."""
scale, rotation, translation = m.get_scale_rotation_translation()
self.scale = scale
self.rotation.m_11 = rotation.m_11
self.rotation.m_12 = rotation.m_12
self.rotation.m_13 = rotation.m_13
self.rotation.m_21 = rotation.m_21
self.rotation.m_22 = rotation.m_22
self.rotation.m_23 = rotation.m_23
self.rotation.m_31 = rotation.m_31
self.rotation.m_32 = rotation.m_32
self.rotation.m_33 = rotation.m_33
self.translation.x = translation.x
self.translation.y = translation.y
self.translation.z = translation.z
def apply_scale(self, scale):
"""Apply scale factor on data.
:param scale: The scale factor."""
# apply scale on translation
self.translation.x *= scale
self.translation.y *= scale
self.translation.z *= scale
# apply scale on bounding box
self.bounding_box.translation.x *= scale
self.bounding_box.translation.y *= scale
self.bounding_box.translation.z *= scale
self.bounding_box.radius.x *= scale
self.bounding_box.radius.y *= scale
self.bounding_box.radius.z *= scale
class NiBSplineCompTransformInterpolator:
def get_translations(self):
"""Return an iterator over all translation keys."""
return self._getCompKeys(self.translation_offset, 3,
self.translation_bias, self.translation_multiplier)
def get_rotations(self):
"""Return an iterator over all rotation keys."""
return self._getCompKeys(self.rotation_offset, 4,
self.rotation_bias, self.rotation_multiplier)
def get_scales(self):
"""Return an iterator over all scale keys."""
for key in self._getCompKeys(self.scale_offset, 1,
self.scale_bias, self.scale_multiplier):
yield key[0]
def apply_scale(self, scale):
"""Apply scale factor on data."""
self.translation.x *= scale
self.translation.y *= scale
self.translation.z *= scale
self.translation_bias *= scale
self.translation_multiplier *= scale
class NiBSplineData:
"""
>>> # a doctest
>>> from pyffi.formats.nif import NifFormat
>>> block = NifFormat.NiBSplineData()
>>> block.num_short_control_points = 50
>>> block.short_control_points.update_size()
>>> for i in range(block.num_short_control_points):
... block.short_control_points[i] = 20 - i
>>> list(block.get_short_data(12, 4, 3))
[(8, 7, 6), (5, 4, 3), (2, 1, 0), (-1, -2, -3)]
>>> offset = block.append_short_data([(1,2),(4,3),(13,14),(8,2),(33,33)])
>>> offset
50
>>> list(block.get_short_data(offset, 5, 2))
[(1, 2), (4, 3), (13, 14), (8, 2), (33, 33)]
>>> list(block.get_comp_data(offset, 5, 2, 10.0, 32767.0))
[(11.0, 12.0), (14.0, 13.0), (23.0, 24.0), (18.0, 12.0), (43.0, 43.0)]
>>> block.append_float_data([(1.0,2.0),(3.0,4.0),(0.5,0.25)])
0
>>> list(block.get_float_data(0, 3, 2))
[(1.0, 2.0), (3.0, 4.0), (0.5, 0.25)]
>>> block.append_comp_data([(1,2),(4,3)])
(60, 2.5, 1.5)
>>> list(block.get_short_data(60, 2, 2))
[(-32767, -10922), (32767, 10922)]
>>> list(block.get_comp_data(60, 2, 2, 2.5, 1.5)) # doctest: +ELLIPSIS
[(1.0, 2.00...), (4.0, 2.99...)]
"""
def _getData(self, offset, num_elements, element_size, controlpoints):
"""Helper function for get_float_data and get_short_data. For internal
use only."""
# check arguments
if not (controlpoints is self.float_control_points
or controlpoints is self.short_control_points):
raise ValueError("internal error while appending data")
# parse the data
for element in xrange(num_elements):
yield tuple(
controlpoints[offset + element * element_size + index]
for index in xrange(element_size))
def _appendData(self, data, controlpoints):
"""Helper function for append_float_data and append_short_data. For internal
use only."""
# get number of elements
num_elements = len(data)
# empty list, do nothing
if num_elements == 0:
return
# get element size
element_size = len(data[0])
# store offset at which we append the data
if controlpoints is self.float_control_points:
offset = self.num_float_control_points
self.num_float_control_points += num_elements * element_size
elif controlpoints is self.short_control_points:
offset = self.num_short_control_points
self.num_short_control_points += num_elements * element_size
else:
raise ValueError("internal error while appending data")
# update size
controlpoints.update_size()
# store the data
for element, datum in enumerate(data):
for index, value in enumerate(datum):
controlpoints[offset + element * element_size + index] = value
# return the offset
return offset
def get_short_data(self, offset, num_elements, element_size):
"""Get an iterator to the data.
:param offset: The offset in the data where to start.
:param num_elements: Number of elements to get.
:param element_size: Size of a single element.
:return: A list of C{num_elements} tuples of size C{element_size}.
"""
return self._getData(
offset, num_elements, element_size, self.short_control_points)
def get_comp_data(self, offset, num_elements, element_size, bias, multiplier):
"""Get an interator to the data, converted to float with extra bias and
multiplication factor. If C{x} is the short value, then the returned value
is C{bias + x * multiplier / 32767.0}.
:param offset: The offset in the data where to start.
:param num_elements: Number of elements to get.
:param element_size: Size of a single element.
:param bias: Value bias.
:param multiplier: Value multiplier.
:return: A list of C{num_elements} tuples of size C{element_size}.
"""
for key in self.get_short_data(offset, num_elements, element_size):
yield tuple(bias + x * multiplier / 32767.0 for x in key)
def append_short_data(self, data):
"""Append data.
:param data: A list of elements, where each element is a tuple of
integers. (Note: cannot be an interator; maybe this restriction
will be removed in a future version.)
:return: The offset at which the data was appended."""
return self._appendData(data, self.short_control_points)
def append_comp_data(self, data):
"""Append data as compressed list.
:param data: A list of elements, where each element is a tuple of
integers. (Note: cannot be an interator; maybe this restriction
will be removed in a future version.)
:return: The offset, bias, and multiplier."""
# get extremes
maxvalue = max(max(datum) for datum in data)
minvalue = min(min(datum) for datum in data)
# get bias and multiplier
bias = 0.5 * (maxvalue + minvalue)
if maxvalue > minvalue:
multiplier = 0.5 * (maxvalue - minvalue)
else:
# no need to compress in this case
multiplier = 1.0
# compress points into shorts
shortdata = []
for datum in data:
shortdata.append(tuple(int(32767 * (x - bias) / multiplier)
for x in datum))
return (self._appendData(shortdata, self.short_control_points),
bias, multiplier)
def get_float_data(self, offset, num_elements, element_size):
"""Get an iterator to the data.
:param offset: The offset in the data where to start.
:param num_elements: Number of elements to get.
:param element_size: Size of a single element.
:return: A list of C{num_elements} tuples of size C{element_size}.
"""
return self._getData(
offset, num_elements, element_size, self.float_control_points)
def append_float_data(self, data):
"""Append data.
:param data: A list of elements, where each element is a tuple of
floats. (Note: cannot be an interator; maybe this restriction
will be removed in a future version.)
:return: The offset at which the data was appended."""
return self._appendData(data, self.float_control_points)
class NiBSplineInterpolator:
def get_times(self):
"""Return an iterator over all key times.
@todo: When code for calculating the bsplines is ready, this function
will return exactly self.basis_data.num_control_points - 1 time points, and
not self.basis_data.num_control_points as it is now.
"""
for i in xrange(self.basis_data.num_control_points):
yield self.start_time + (i * (self.stop_time - self.start_time)
/ (self.basis_data.num_control_points - 1))
def _getFloatKeys(self, offset, element_size):
"""Helper function to get iterator to various keys. Internal use only."""
# are there keys?
if offset == 65535:
return
# yield all keys
for key in self.spline_data.get_float_data(offset,
self.basis_data.num_control_points,
element_size):
yield key
def _getCompKeys(self, offset, element_size, bias, multiplier):
"""Helper function to get iterator to various keys. Internal use only."""
# are there keys?
if offset == 65535:
return
# yield all keys
for key in self.spline_data.get_comp_data(offset,
self.basis_data.num_control_points,
element_size,
bias, multiplier):
yield key
class NiBSplineTransformInterpolator:
def get_translations(self):
"""Return an iterator over all translation keys."""
return self._getFloatKeys(self.translation_offset, 3)
def get_rotations(self):
"""Return an iterator over all rotation keys."""
return self._getFloatKeys(self.rotation_offset, 4)
def get_scales(self):
"""Return an iterator over all scale keys."""
for key in self._getFloatKeys(self.scale_offset, 1):
yield key[0]
def apply_scale(self, scale):
"""Apply scale factor on data."""
self.translation.x *= scale
self.translation.y *= scale
self.translation.z *= scale
# also scale translation float keys
if self.translation_offset != 65535:
offset = self.translation_offset
num_elements = self.basis_data.num_control_points
element_size = 3
controlpoints = self.spline_data.float_control_points
for element in xrange(num_elements):
for index in xrange(element_size):
controlpoints[offset + element * element_size + index] *= scale
class NiControllerSequence:
def add_controlled_block(self):
"""Create new controlled block, and return it.
>>> seq = NifFormat.NiControllerSequence()
>>> seq.num_controlled_blocks
0
>>> ctrlblock = seq.add_controlled_block()
>>> seq.num_controlled_blocks
1
>>> isinstance(ctrlblock, NifFormat.ControllerLink)
True
"""
# add to the list
num_blocks = self.num_controlled_blocks
self.num_controlled_blocks = num_blocks + 1
self.controlled_blocks.update_size()
return self.controlled_blocks[-1]
class NiGeometryData:
"""
>>> from pyffi.formats.nif import NifFormat
>>> geomdata = NifFormat.NiGeometryData()
>>> geomdata.num_vertices = 3
>>> geomdata.has_vertices = True
>>> geomdata.has_normals = True
>>> geomdata.has_vertex_colors = True
>>> geomdata.num_uv_sets = 2
>>> geomdata.vertices.update_size()
>>> geomdata.normals.update_size()
>>> geomdata.vertex_colors.update_size()
>>> geomdata.uv_sets.update_size()
>>> geomdata.vertices[0].x = 1
>>> geomdata.vertices[0].y = 2
>>> geomdata.vertices[0].z = 3
>>> geomdata.vertices[1].x = 4
>>> geomdata.vertices[1].y = 5
>>> geomdata.vertices[1].z = 6
>>> geomdata.vertices[2].x = 1.200001
>>> geomdata.vertices[2].y = 3.400001
>>> geomdata.vertices[2].z = 5.600001
>>> geomdata.normals[0].x = 0
>>> geomdata.normals[0].y = 0
>>> geomdata.normals[0].z = 1
>>> geomdata.normals[1].x = 0
>>> geomdata.normals[1].y = 1
>>> geomdata.normals[1].z = 0
>>> geomdata.normals[2].x = 1
>>> geomdata.normals[2].y = 0
>>> geomdata.normals[2].z = 0
>>> geomdata.vertex_colors[1].r = 0.310001
>>> geomdata.vertex_colors[1].g = 0.320001
>>> geomdata.vertex_colors[1].b = 0.330001
>>> geomdata.vertex_colors[1].a = 0.340001
>>> geomdata.uv_sets[0][0].u = 0.990001
>>> geomdata.uv_sets[0][0].v = 0.980001
>>> geomdata.uv_sets[0][2].u = 0.970001
>>> geomdata.uv_sets[0][2].v = 0.960001
>>> geomdata.uv_sets[1][0].v = 0.910001
>>> geomdata.uv_sets[1][0].v = 0.920001
>>> geomdata.uv_sets[1][2].v = 0.930001
>>> geomdata.uv_sets[1][2].v = 0.940001
>>> for h in geomdata.get_vertex_hash_generator():
... print(h)
(1000, 2000, 3000, 0, 0, 1000, 99000, 98000, 0, 92000, 0, 0, 0, 0)
(4000, 5000, 6000, 0, 1000, 0, 0, 0, 0, 0, 310, 320, 330, 340)
(1200, 3400, 5600, 1000, 0, 0, 97000, 96000, 0, 94000, 0, 0, 0, 0)
"""
def update_center_radius(self):
"""Recalculate center and radius of the data."""
# in case there are no vertices, set center and radius to zero
if len(self.vertices) == 0:
self.center.x = 0.0
self.center.y = 0.0
self.center.z = 0.0
self.radius = 0.0
return
# find extreme values in x, y, and z direction
lowx = min([v.x for v in self.vertices])
lowy = min([v.y for v in self.vertices])
lowz = min([v.z for v in self.vertices])
highx = max([v.x for v in self.vertices])
highy = max([v.y for v in self.vertices])
highz = max([v.z for v in self.vertices])
# center is in the center of the bounding box
cx = (lowx + highx) * 0.5
cy = (lowy + highy) * 0.5
cz = (lowz + highz) * 0.5
self.center.x = cx
self.center.y = cy
self.center.z = cz
# radius is the largest distance from the center
r2 = 0.0
for v in self.vertices:
dx = cx - v.x
dy = cy - v.y
dz = cz - v.z
r2 = max(r2, dx*dx+dy*dy+dz*dz)
self.radius = r2 ** 0.5
def apply_scale(self, scale):
"""Apply scale factor on data."""
if abs(scale - 1.0) < NifFormat.EPSILON: return
for v in self.vertices:
v.x *= scale
v.y *= scale
v.z *= scale
self.center.x *= scale
self.center.y *= scale
self.center.z *= scale
self.radius *= scale
def get_vertex_hash_generator(
self,
vertexprecision=3, normalprecision=3,
uvprecision=5, vcolprecision=3):
"""Generator which produces a tuple of integers for each
(vertex, normal, uv, vcol), to ease detection of duplicate
vertices. The precision parameters denote number of
significant digits behind the comma.
Default for uvprecision should really be high because for
very large models the uv coordinates can be very close
together.
For vertexprecision, 3 seems usually enough (maybe we'll
have to increase this at some point).
:param vertexprecision: Precision to be used for vertices.
:type vertexprecision: float
:param normalprecision: Precision to be used for normals.
:type normalprecision: float
:param uvprecision: Precision to be used for uvs.
:type uvprecision: float
:param vcolprecision: Precision to be used for vertex colors.
:type vcolprecision: float
:return: A generator yielding a hash value for each vertex.
"""
verts = self.vertices if self.has_vertices else None
norms = self.normals if self.has_normals else None
uvsets = self.uv_sets if len(self.uv_sets) else None
vcols = self.vertex_colors if self.has_vertex_colors else None
vertexfactor = 10 ** vertexprecision
normalfactor = 10 ** normalprecision
uvfactor = 10 ** uvprecision
vcolfactor = 10 ** vcolprecision
for i in xrange(self.num_vertices):
h = []
if verts:
h.extend([float_to_int(x * vertexfactor)
for x in [verts[i].x, verts[i].y, verts[i].z]])
if norms:
h.extend([float_to_int(x * normalfactor)
for x in [norms[i].x, norms[i].y, norms[i].z]])
if uvsets:
for uvset in uvsets:
# uvs sometimes have NaN, for example:
# oblivion/meshes/architecture/anvil/anvildooruc01.nif
h.extend([float_to_int(x * uvfactor)
for x in [uvset[i].u, uvset[i].v]])
if vcols:
h.extend([float_to_int(x * vcolfactor)
for x in [vcols[i].r, vcols[i].g,
vcols[i].b, vcols[i].a]])
yield tuple(h)
class NiGeometry:
"""
>>> from pyffi.formats.nif import NifFormat
>>> id44 = NifFormat.Matrix44()
>>> id44.set_identity()
>>> skelroot = NifFormat.NiNode()
>>> skelroot.name = 'skelroot'
>>> skelroot.set_transform(id44)
>>> bone1 = NifFormat.NiNode()
>>> bone1.name = 'bone1'
>>> bone1.set_transform(id44)
>>> bone2 = NifFormat.NiNode()
>>> bone2.name = 'bone2'
>>> bone2.set_transform(id44)
>>> bone21 = NifFormat.NiNode()
>>> bone21.name = 'bone21'
>>> bone21.set_transform(id44)
>>> bone22 = NifFormat.NiNode()
>>> bone22.name = 'bone22'
>>> bone22.set_transform(id44)
>>> bone211 = NifFormat.NiNode()
>>> bone211.name = 'bone211'
>>> bone211.set_transform(id44)
>>> skelroot.add_child(bone1)
>>> bone1.add_child(bone2)
>>> bone2.add_child(bone21)
>>> bone2.add_child(bone22)
>>> bone21.add_child(bone211)
>>> geom = NifFormat.NiTriShape()
>>> geom.name = 'geom'
>>> geom.set_transform(id44)
>>> geomdata = NifFormat.NiTriShapeData()
>>> skininst = NifFormat.NiSkinInstance()
>>> skindata = NifFormat.NiSkinData()
>>> skelroot.add_child(geom)
>>> geom.data = geomdata
>>> geom.skin_instance = skininst
>>> skininst.skeleton_root = skelroot
>>> skininst.data = skindata
>>> skininst.num_bones = 4
>>> skininst.bones.update_size()
>>> skininst.bones[0] = bone1
>>> skininst.bones[1] = bone2
>>> skininst.bones[2] = bone22
>>> skininst.bones[3] = bone211
>>> skindata.num_bones = 4
>>> skindata.bone_list.update_size()
>>> [child.name for child in skelroot.children]
['bone1', 'geom']
>>> skindata.set_transform(id44)
>>> for bonedata in skindata.bone_list:
... bonedata.set_transform(id44)
>>> affectedbones = geom.flatten_skin()
>>> [bone.name for bone in affectedbones]
['bone1', 'bone2', 'bone22', 'bone211']
>>> [child.name for child in skelroot.children]
['geom', 'bone1', 'bone21', 'bone2', 'bone22', 'bone211']
"""
def is_skin(self):
"""Returns True if geometry is skinned."""
return self.skin_instance != None
def _validateSkin(self):
"""Check that skinning blocks are valid. Will raise NifError exception
if not."""
if self.skin_instance == None: return
if self.skin_instance.data == None:
raise NifFormat.NifError('NiGeometry has NiSkinInstance without NiSkinData')
if self.skin_instance.skeleton_root == None:
raise NifFormat.NifError('NiGeometry has NiSkinInstance without skeleton root')
if self.skin_instance.num_bones != self.skin_instance.data.num_bones:
raise NifFormat.NifError('NiSkinInstance and NiSkinData have different number of bones')
def add_bone(self, bone, vert_weights):
"""Add bone with given vertex weights.
After adding all bones, the geometry skinning information should be set
from the current position of the bones using the L{update_bind_position} function.
:param bone: The bone NiNode block.
:param vert_weights: A dictionary mapping each influenced vertex index to a vertex weight."""
self._validateSkin()
skininst = self.skin_instance
skindata = skininst.data
skelroot = skininst.skeleton_root
bone_index = skininst.num_bones
skininst.num_bones = bone_index+1
skininst.bones.update_size()
skininst.bones[bone_index] = bone
skindata.num_bones = bone_index+1
skindata.bone_list.update_size()
skinbonedata = skindata.bone_list[bone_index]
# set vertex weights
skinbonedata.num_vertices = len(vert_weights)
skinbonedata.vertex_weights.update_size()
for i, (vert_index, vert_weight) in enumerate(vert_weights.iteritems()):
skinbonedata.vertex_weights[i].index = vert_index
skinbonedata.vertex_weights[i].weight = vert_weight
def get_vertex_weights(self):
"""Get vertex weights in a convenient format: list bone and weight per
vertex."""
# shortcuts relevant blocks
if not self.skin_instance:
raise NifFormat.NifError('Cannot get vertex weights of geometry without skin.')
self._validateSkin()
geomdata = self.data
skininst = self.skin_instance
skindata = skininst.data
# XXX todo: should we use list of dictionaries for this
# where each dict maps bone number to the weight?
weights = [[] for i in xrange(geomdata.num_vertices)]
for bonenum, bonedata in enumerate(skindata.bone_list):
for skinweight in bonedata.vertex_weights:
# skip zero weights
if skinweight.weight != 0:
# boneweightlist is the list of (bonenum, weight) pairs that
# we must update now
boneweightlist = weights[skinweight.index]
# is bonenum already in there?
for i, (otherbonenum, otherweight) in enumerate(boneweightlist):
if otherbonenum == bonenum:
# yes! add the weight to the bone
boneweightlist[i][1] += skinweight.weight
break
else:
# nope... so add new [bone, weight] entry
boneweightlist.append([bonenum, skinweight.weight])
return weights
def flatten_skin(self):
"""Reposition all bone blocks and geometry block in the tree to be direct
children of the skeleton root.
Returns list of all used bones by the skin."""
if not self.is_skin(): return [] # nothing to do
result = [] # list of repositioned bones
self._validateSkin() # validate the skin
skininst = self.skin_instance
skindata = skininst.data
skelroot = skininst.skeleton_root
# reparent geometry
self.set_transform(self.get_transform(skelroot))
geometry_parent = skelroot.find_chain(self, block_type = NifFormat.NiAVObject)[-2]
geometry_parent.remove_child(self) # detatch geometry from tree
skelroot.add_child(self, front = True) # and attatch it to the skeleton root
# reparent all the bone blocks
for bone_block in skininst.bones:
# skeleton root, if it is used as bone, does not need to be processed
if bone_block == skelroot: continue
# get bone parent
bone_parent = skelroot.find_chain(bone_block, block_type = NifFormat.NiAVObject)[-2]
# set new child transforms
for child in bone_block.children:
child.set_transform(child.get_transform(bone_parent))
# reparent children
for child in bone_block.children:
bone_parent.add_child(child)
bone_block.num_children = 0
bone_block.children.update_size() # = remove_child on each child
# set new bone transform
bone_block.set_transform(bone_block.get_transform(skelroot))
# reparent bone block
bone_parent.remove_child(bone_block)
skelroot.add_child(bone_block)
result.append(bone_block)
return result
# The nif skinning algorithm works as follows (as of nifskope):
# v' # vertex after skinning in geometry space
# = sum over {b in skininst.bones} # sum over all bones b that influence the mesh
# weight[v][b] # how much bone b influences vertex v
# * v # vertex before skinning in geometry space (as it is stored in the shape data)
# * skindata.bone_list[b].transform # transform vertex to bone b space in the rest pose
# * b.get_transform(skelroot) # apply animation, by multiplying with all bone matrices in the chain down to the skeleton root; the vertex is now in skeleton root space
# * skindata.transform # transforms vertex from skeleton root space back to geometry space
def get_skin_deformation(self):
"""Returns a list of vertices and normals in their final position after
skinning, in geometry space."""
if not self.data: return [], []
if not self.is_skin(): return self.data.vertices, self.data.normals
self._validateSkin()
skininst = self.skin_instance
skindata = skininst.data
skelroot = skininst.skeleton_root
vertices = [ NifFormat.Vector3() for i in xrange(self.data.num_vertices) ]
normals = [ NifFormat.Vector3() for i in xrange(self.data.num_vertices) ]
sumweights = [ 0.0 for i in xrange(self.data.num_vertices) ]
skin_offset = skindata.get_transform()
for i, bone_block in enumerate(skininst.bones):
bonedata = skindata.bone_list[i]
bone_offset = bonedata.get_transform()
bone_matrix = bone_block.get_transform(skelroot)
transform = bone_offset * bone_matrix * skin_offset
scale, rotation, translation = transform.get_scale_rotation_translation()
for skinweight in bonedata.vertex_weights:
index = skinweight.index
weight = skinweight.weight
vertices[index] += weight * (self.data.vertices[index] * transform)
if self.data.has_normals:
normals[index] += weight * (self.data.normals[index] * rotation)
sumweights[index] += weight
for i, s in enumerate(sumweights):
if abs(s - 1.0) > 0.01:
logging.getLogger("pyffi.nif.nigeometry").warn(
"vertex %i has weights not summing to one" % i)
return vertices, normals
# ported and extended from niflib::NiNode::GoToSkeletonBindPosition() (r2518)
def send_bones_to_bind_position(self):
"""Send all bones to their bind position.
@deprecated: Use L{NifFormat.NiNode.send_bones_to_bind_position} instead of
this function.
"""
warnings.warn("use NifFormat.NiNode.send_bones_to_bind_position",
DeprecationWarning)
if not self.is_skin():
return
# validate skin and set up quick links
self._validateSkin()
skininst = self.skin_instance
skindata = skininst.data
skelroot = skininst.skeleton_root
# reposition the bones
for i, parent_bone in enumerate(skininst.bones):
parent_offset = skindata.bone_list[i].get_transform()
# if parent_bone is a child of the skeleton root, then fix its
# transfrom
if parent_bone in skelroot.children:
parent_bone.set_transform(parent_offset.get_inverse() * self.get_transform(skelroot))
# fix the transform of all its children
for j, child_bone in enumerate(skininst.bones):
if child_bone not in parent_bone.children: continue
child_offset = skindata.bone_list[j].get_transform()
child_matrix = child_offset.get_inverse() * parent_offset
child_bone.set_transform(child_matrix)
# ported from niflib::NiSkinData::ResetOffsets (r2561)
def update_bind_position(self):
"""Make current position of the bones the bind position for this geometry.
Sets the NiSkinData overall transform to the inverse of the geometry transform
relative to the skeleton root, and sets the NiSkinData of each bone to
the geometry transform relative to the skeleton root times the inverse of the bone
transform relative to the skeleton root."""
if not self.is_skin(): return
# validate skin and set up quick links
self._validateSkin()
skininst = self.skin_instance
skindata = skininst.data
skelroot = skininst.skeleton_root
# calculate overall offset
geomtransform = self.get_transform(skelroot)
skindata.set_transform(geomtransform.get_inverse())
# calculate bone offsets
for i, bone in enumerate(skininst.bones):
skindata.bone_list[i].set_transform(geomtransform * bone.get_transform(skelroot).get_inverse())
def get_skin_partition(self):
"""Return the skin partition block."""
skininst = self.skin_instance
if not skininst:
skinpart = None
else:
skinpart = skininst.skin_partition
if not skinpart:
skindata = skininst.data
if skindata:
skinpart = skindata.skin_partition
return skinpart
def set_skin_partition(self, skinpart):
"""Set skin partition block."""
skininst = self.skin_instance
if not skininst:
raise ValueError("Geometry has no skin instance.")
skindata = skininst.data
if not skindata:
raise ValueError("Geometry has no skin data.")
skininst.skin_partition = skinpart
skindata.skin_partition = skinpart
class NiKeyframeData:
def apply_scale(self, scale):
"""Apply scale factor on data."""
for key in self.translations.keys:
key.value.x *= scale
key.value.y *= scale
key.value.z *= scale
#key.forward.x *= scale
#key.forward.y *= scale
#key.forward.z *= scale
#key.backward.x *= scale
#key.backward.y *= scale
#key.backward.z *= scale
# what to do with TBC?
class NiMaterialColorController:
def get_target_color(self):
"""Get target color (works for all nif versions)."""
return ((self.flags >> 4) & 7) | self.target_color
def set_target_color(self, target_color):
"""Set target color (works for all nif versions)."""
self.flags |= (target_color & 7) << 4
self.target_color = target_color
class NiMorphData:
def apply_scale(self, scale):
"""Apply scale factor on data."""
for morph in self.morphs:
for v in morph.vectors:
v.x *= scale
v.y *= scale
v.z *= scale
class NiNode:
"""
>>> from pyffi.formats.nif import NifFormat
>>> x = NifFormat.NiNode()
>>> y = NifFormat.NiNode()
>>> z = NifFormat.NiNode()
>>> x.num_children =1
>>> x.children.update_size()
>>> y in x.children
False
>>> x.children[0] = y
>>> y in x.children
True
>>> x.add_child(z, front = True)
>>> x.add_child(y)
>>> x.num_children
2
>>> x.children[0] is z
True
>>> x.remove_child(y)
>>> y in x.children
False
>>> x.num_children
1
>>> e = NifFormat.NiSpotLight()
>>> x.add_effect(e)
>>> x.num_effects
1
>>> e in x.effects
True
>>> from pyffi.formats.nif import NifFormat
>>> node = NifFormat.NiNode()
>>> child1 = NifFormat.NiNode()
>>> child1.name = "hello"
>>> child_2 = NifFormat.NiNode()
>>> child_2.name = "world"
>>> node.get_children()
[]
>>> node.set_children([child1, child_2])
>>> [child.name for child in node.get_children()]
['hello', 'world']
>>> [child.name for child in node.children]
['hello', 'world']
>>> node.set_children([])
>>> node.get_children()
[]
>>> # now set them the other way around
>>> node.set_children([child_2, child1])
>>> [child.name for child in node.get_children()]
['world', 'hello']
>>> [child.name for child in node.children]
['world', 'hello']
>>> node.remove_child(child_2)
>>> [child.name for child in node.children]
['hello']
>>> node.add_child(child_2)
>>> [child.name for child in node.children]
['hello', 'world']
>>> from pyffi.formats.nif import NifFormat
>>> node = NifFormat.NiNode()
>>> effect1 = NifFormat.NiSpotLight()
>>> effect1.name = "hello"
>>> effect2 = NifFormat.NiSpotLight()
>>> effect2.name = "world"
>>> node.get_effects()
[]
>>> node.set_effects([effect1, effect2])
>>> [effect.name for effect in node.get_effects()]
['hello', 'world']
>>> [effect.name for effect in node.effects]
['hello', 'world']
>>> node.set_effects([])
>>> node.get_effects()
[]
>>> # now set them the other way around
>>> node.set_effects([effect2, effect1])
>>> [effect.name for effect in node.get_effects()]
['world', 'hello']
>>> [effect.name for effect in node.effects]
['world', 'hello']
>>> node.remove_effect(effect2)
>>> [effect.name for effect in node.effects]
['hello']
>>> node.add_effect(effect2)
>>> [effect.name for effect in node.effects]
['hello', 'world']
"""
def add_child(self, child, front=False):
"""Add block to child list.
:param child: The child to add.
:type child: L{NifFormat.NiAVObject}
:keyword front: Whether to add to the front or to the end of the
list (default is at end).
:type front: ``bool``
"""
# check if it's already a child
if child in self.children:
return
# increase number of children
num_children = self.num_children
self.num_children = num_children + 1
self.children.update_size()
# add the child
if not front:
self.children[num_children] = child
else:
for i in xrange(num_children, 0, -1):
self.children[i] = self.children[i-1]
self.children[0] = child
def remove_child(self, child):
"""Remove a block from the child list.
:param child: The child to remove.
:type child: L{NifFormat.NiAVObject}
"""
self.set_children([otherchild for otherchild in self.get_children()
if not(otherchild is child)])
def get_children(self):
"""Return a list of the children of the block.
:return: The list of children.
:rtype: ``list`` of L{NifFormat.NiAVObject}
"""
return [child for child in self.children]
def set_children(self, childlist):
"""Set the list of children from the given list (destroys existing list).
:param childlist: The list of child blocks to set.
:type childlist: ``list`` of L{NifFormat.NiAVObject}
"""
self.num_children = len(childlist)
self.children.update_size()
for i, child in enumerate(childlist):
self.children[i] = child
def add_effect(self, effect):
"""Add an effect to the list of effects.
:param effect: The effect to add.
:type effect: L{NifFormat.NiDynamicEffect}
"""
num_effs = self.num_effects
self.num_effects = num_effs + 1
self.effects.update_size()
self.effects[num_effs] = effect
def remove_effect(self, effect):
"""Remove a block from the effect list.
:param effect: The effect to remove.
:type effect: L{NifFormat.NiDynamicEffect}
"""
self.set_effects([othereffect for othereffect in self.get_effects()
if not(othereffect is effect)])
def get_effects(self):
"""Return a list of the effects of the block.
:return: The list of effects.
:rtype: ``list`` of L{NifFormat.NiDynamicEffect}
"""
return [effect for effect in self.effects]
def set_effects(self, effectlist):
"""Set the list of effects from the given list (destroys existing list).
:param effectlist: The list of effect blocks to set.
:type effectlist: ``list`` of L{NifFormat.NiDynamicEffect}
"""
self.num_effects = len(effectlist)
self.effects.update_size()
for i, effect in enumerate(effectlist):
self.effects[i] = effect
def merge_external_skeleton_root(self, skelroot):
"""Attach skinned geometry to self (which will be the new skeleton root of
the nif at the given skeleton root). Use this function if you move a
skinned geometry from one nif into a new nif file. The bone links will be
updated to point to the tree at self, instead of to the external tree.
"""
# sanity check
if self.name != skelroot.name:
raise ValueError("skeleton root names do not match")
# get a dictionary mapping bone names to bone blocks
bone_dict = {}
for block in self.tree():
if isinstance(block, NifFormat.NiNode):
if block.name:
if block.name in bone_dict:
raise ValueError(
"multiple NiNodes with name %s" % block.name)
bone_dict[block.name] = block
# add all non-bone children of the skeleton root to self
for child in skelroot.get_children():
# skip empty children
if not child:
continue
# skip bones
if child.name in bone_dict:
continue
# not a bone, so add it
self.add_child(child)
# fix links to skeleton root and bones
for externalblock in child.tree():
if isinstance(externalblock, NifFormat.NiSkinInstance):
if not(externalblock.skeleton_root is skelroot):
raise ValueError(
"expected skeleton root %s but got %s"
% (skelroot.name, externalblock.skeleton_root.name))
externalblock.skeleton_root = self
for i, externalbone in enumerate(externalblock.bones):
externalblock.bones[i] = bone_dict[externalbone.name]
def merge_skeleton_roots(self):
"""This function will look for other geometries whose skeleton
root is a (possibly indirect) child of this node. It will then
reparent those geometries to this node. For example, it will unify
the skeleton roots in Morrowind's cliffracer.nif file, or of the
(official) body skins. This makes it much easier to import
skeletons in for instance Blender: there will be only one skeleton
root for each bone, over all geometries.
The merge fails for those geometries whose global skin data
transform does not match the inverse geometry transform relative to
the skeleton root (the maths does not work out in this case!)
Returns list of all new blocks that have been reparented (and
added to the skeleton root children list), and a list of blocks
for which the merge failed.
"""
logger = logging.getLogger("pyffi.nif.ninode")
result = [] # list of reparented blocks
failed = [] # list of blocks that could not be reparented
id44 = NifFormat.Matrix44()
id44.set_identity()
# find the root block (direct parent of skeleton root that connects to the geometry) for each of these geometries
for geom in self.get_global_iterator():
# make sure we only do each geometry once
if (geom in result) or (geom in failed):
continue
# only geometries
if not isinstance(geom, NifFormat.NiGeometry):
continue
# only skins
if not geom.is_skin():
continue
# only if they have a different skeleton root
if geom.skin_instance.skeleton_root is self:
continue
# check transforms
if (geom.skin_instance.data.get_transform()
* geom.get_transform(geom.skin_instance.skeleton_root) != id44):
logger.warn(
"can't rebase %s: global skin data transform does not match "
"geometry transform relative to skeleton root" % geom.name)
failed.append(geom)
continue # skip this one
# everything ok!
# find geometry parent
geomroot = geom.skin_instance.skeleton_root.find_chain(geom)[-2]
# reparent
logger.debug("detaching %s from %s" % (geom.name, geomroot.name))
geomroot.remove_child(geom)
logger.debug("attaching %s to %s" % (geom.name, self.name))
self.add_child(geom)
# set its new skeleton root
geom.skin_instance.skeleton_root = self
# fix transform
geom.skin_instance.data.set_transform(
geom.get_transform(self).get_inverse(fast=False))
# and signal that we reparented this block
result.append(geom)
return result, failed
def get_skinned_geometries(self):
"""This function yields all skinned geometries which have self as
skeleton root.
"""
for geom in self.get_global_iterator():
if (isinstance(geom, NifFormat.NiGeometry)
and geom.is_skin()
and geom.skin_instance.skeleton_root is self):
yield geom
def send_geometries_to_bind_position(self):
"""Call this on the skeleton root of geometries. This function will
transform the geometries, such that all skin data transforms coincide, or
at least coincide partially.
:return: A number quantifying the remaining difference between bind
positions.
:rtype: ``float``
"""
# get logger
logger = logging.getLogger("pyffi.nif.ninode")
# maps bone name to bind position transform matrix (relative to
# skeleton root)
bone_bind_transform = {}
# find all skinned geometries with self as skeleton root
geoms = list(self.get_skinned_geometries())
# sort geometries by bone level
# this ensures that "parent" geometries serve as reference for "child"
# geometries
sorted_geoms = []
for bone in self.get_global_iterator():
if not isinstance(bone, NifFormat.NiNode):
continue
for geom in geoms:
if not geom in sorted_geoms:
if bone in geom.skin_instance.bones:
sorted_geoms.append(geom)
geoms = sorted_geoms
# now go over all geometries and synchronize their relative bind poses
for geom in geoms:
skininst = geom.skin_instance
skindata = skininst.data
# set difference matrix to identity
diff = NifFormat.Matrix44()
diff.set_identity()
# go over all bones in current geometry, see if it has been visited
# before
for bonenode, bonedata in izip(skininst.bones, skindata.bone_list):
if bonenode.name in bone_bind_transform:
# calculate difference
# (see explanation below)
diff = (bonedata.get_transform()
* bone_bind_transform[bonenode.name]
* geom.get_transform(self).get_inverse(fast=False))
break
if diff.is_identity():
logger.debug("%s is already in bind position" % geom.name)
else:
logger.info("fixing %s bind position" % geom.name)
# explanation:
# we must set the bonedata transform T' such that its bone bind
# position matrix
# T'^-1 * G
# (where T' = the updated bonedata.get_transform()
# and G = geom.get_transform(self))
# coincides with the desired matrix
# B = bone_bind_transform[bonenode.name]
# in other words:
# T' = G * B^-1
# or, with diff = D = T * B * G^-1
# T' = D^-1 * T
# to keep the geometry in sync, the vertices and normals must
# be multiplied with D, e.g. v' = v * D
# because the full transform
# v * T * ... = v * D * D^-1 * T * ... = v' * T' * ...
# must be kept invariant
for bonenode, bonedata in izip(skininst.bones, skindata.bone_list):
logger.debug("transforming bind position of bone %s"
% bonenode.name)
bonedata.set_transform(diff.get_inverse(fast=False)
* bonedata.get_transform())
# transform geometry
logger.debug("transforming vertices and normals")
for vert in geom.data.vertices:
newvert = vert * diff
vert.x = newvert.x
vert.y = newvert.y
vert.z = newvert.z
for norm in geom.data.normals:
newnorm = norm * diff.get_matrix_33()
norm.x = newnorm.x
norm.y = newnorm.y
norm.z = newnorm.z
# store updated bind position for future reference
for bonenode, bonedata in izip(skininst.bones, skindata.bone_list):
bone_bind_transform[bonenode.name] = (
bonedata.get_transform().get_inverse(fast=False)
* geom.get_transform(self))
# validation: check that bones share bind position
bone_bind_transform = {}
error = 0.0
for geom in geoms:
skininst = geom.skin_instance
skindata = skininst.data
# go over all bones in current geometry, see if it has been visited
# before
for bonenode, bonedata in izip(skininst.bones, skindata.bone_list):
if bonenode.name in bone_bind_transform:
# calculate difference
diff = ((bonedata.get_transform().get_inverse(fast=False)
* geom.get_transform(self))
- bone_bind_transform[bonenode.name])
# calculate error (sup norm)
error = max(error,
max(max(abs(elem) for elem in row)
for row in diff.as_list()))
else:
bone_bind_transform[bonenode.name] = (
bonedata.get_transform().get_inverse(fast=False)
* geom.get_transform(self))
logger.debug("Geometry bind position error is %f" % error)
if error > 1e-3:
logger.warning("Failed to send some geometries to bind position")
return error
def send_detached_geometries_to_node_position(self):
"""Some nifs (in particular in Morrowind) have geometries that are skinned
but that do not share bones. In such cases, send_geometries_to_bind_position
cannot reposition them. This function will send such geometries to the
position of their root node.
Examples of such nifs are the official Morrowind skins (after merging
skeleton roots).
Returns list of detached geometries that have been moved.
"""
logger = logging.getLogger("pyffi.nif.ninode")
geoms = list(self.get_skinned_geometries())
# parts the geometries into sets that do not share bone influences
# * first construct sets of bones, merge intersecting sets
# * then check which geometries belong to which set
bonesets = [list(set(geom.skin_instance.bones)) for geom in geoms]
# the merged flag signals that we are still merging bones
merged = True
while merged:
merged = False
for boneset in bonesets:
for other_boneset in bonesets:
# skip if sets are identical
if other_boneset is boneset:
continue
# if not identical, see if they can be merged
if set(other_boneset) & set(boneset):
# XXX hackish but works
# calculate union
updated_boneset = list(set(other_boneset) | set(boneset))
# and move all bones into one bone set
del other_boneset[:]
del boneset[:]
boneset += updated_boneset
merged = True
# remove empty bone sets
bonesets = list(boneset for boneset in bonesets if boneset)
logger.debug("bones per partition are")
for boneset in bonesets:
logger.debug(str([bone.name for bone in boneset]))
parts = [[geom for geom in geoms
if set(geom.skin_instance.bones) & set(boneset)]
for boneset in bonesets]
logger.debug("geometries per partition are")
for part in parts:
logger.debug(str([geom.name for geom in part]))
# if there is only one set, we are done
if len(bonesets) <= 1:
logger.debug("no detached geometries")
return []
# next, for each part, move all geometries so the lowest bone matches the
# node transform
for boneset, part in izip(bonesets, parts):
logger.debug("moving part %s" % str([geom.name for geom in part]))
# find "lowest" bone in the bone set
lowest_dist = None
lowest_bonenode = None
for bonenode in boneset:
dist = len(self.find_chain(bonenode))
if (lowest_dist is None) or (lowest_dist > dist):
lowest_dist = dist
lowest_bonenode = bonenode
logger.debug("reference bone is %s" % lowest_bonenode.name)
# find a geometry that has this bone
for geom in part:
for bonenode, bonedata in izip(geom.skin_instance.bones,
geom.skin_instance.data.bone_list):
if bonenode is lowest_bonenode:
lowest_geom = geom
lowest_bonedata = bonedata
break
else:
continue
break
else:
raise RuntimeError("no reference geometry with this bone: bug?")
# calculate matrix
diff = (lowest_bonedata.get_transform()
* lowest_bonenode.get_transform(self)
* lowest_geom.get_transform(self).get_inverse(fast=False))
if diff.is_identity():
logger.debug("%s is already in node position"
% lowest_bonenode.name)
continue
# now go over all geometries and synchronize their position to the
# reference bone
for geom in part:
logger.info("moving %s to node position" % geom.name)
# XXX we're using this trick a few times now
# XXX move it to a separate NiGeometry function
skininst = geom.skin_instance
skindata = skininst.data
# explanation:
# we must set the bonedata transform T' such that its bone bind
# position matrix
# T'^-1 * G
# (where T' = the updated lowest_bonedata.get_transform()
# and G = geom.get_transform(self))
# coincides with the desired matrix
# B = lowest_bonenode.get_transform(self)
# in other words:
# T' = G * B^-1
# or, with diff = D = T * B * G^-1
# T' = D^-1 * T
# to keep the geometry in sync, the vertices and normals must
# be multiplied with D, e.g. v' = v * D
# because the full transform
# v * T * ... = v * D * D^-1 * T * ... = v' * T' * ...
# must be kept invariant
for bonenode, bonedata in izip(skininst.bones, skindata.bone_list):
logger.debug("transforming bind position of bone %s"
% bonenode.name)
bonedata.set_transform(diff.get_inverse(fast=False)
* bonedata.get_transform())
# transform geometry
logger.debug("transforming vertices and normals")
for vert in geom.data.vertices:
newvert = vert * diff
vert.x = newvert.x
vert.y = newvert.y
vert.z = newvert.z
for norm in geom.data.normals:
newnorm = norm * diff.get_matrix_33()
norm.x = newnorm.x
norm.y = newnorm.y
norm.z = newnorm.z
def send_bones_to_bind_position(self):
"""This function will send all bones of geometries of this skeleton root
to their bind position. For best results, call
L{send_geometries_to_bind_position} first.
:return: A number quantifying the remaining difference between bind
positions.
:rtype: ``float``
"""
# get logger
logger = logging.getLogger("pyffi.nif.ninode")
# check all bones and bone datas to see if a bind position exists
bonelist = []
error = 0.0
geoms = list(self.get_skinned_geometries())
for geom in geoms:
skininst = geom.skin_instance
skindata = skininst.data
for bonenode, bonedata in izip(skininst.bones, skindata.bone_list):
# make sure all bone data of shared bones coincides
for othergeom, otherbonenode, otherbonedata in bonelist:
if bonenode is otherbonenode:
diff = ((otherbonedata.get_transform().get_inverse(fast=False)
*
othergeom.get_transform(self))
-
(bonedata.get_transform().get_inverse(fast=False)
*
geom.get_transform(self)))
if diff.sup_norm() > 1e-3:
logger.warning("Geometries %s and %s do not share the same bind position: bone %s will be sent to a position matching only one of these" % (geom.name, othergeom.name, bonenode.name))
# break the loop
break
else:
# the loop did not break, so the bone was not yet added
# add it now
logger.debug("Found bind position data for %s" % bonenode.name)
bonelist.append((geom, bonenode, bonedata))
# the algorithm simply makes all transforms correct by changing
# each local bone matrix in such a way that the global matrix
# relative to the skeleton root matches the skinning information
# this algorithm is numerically most stable if bones are traversed
# in hierarchical order, so first sort the bones
sorted_bonelist = []
for node in self.tree():
if not isinstance(node, NifFormat.NiNode):
continue
for geom, bonenode, bonedata in bonelist:
if node is bonenode:
sorted_bonelist.append((geom, bonenode, bonedata))
bonelist = sorted_bonelist
# now reposition the bones
for geom, bonenode, bonedata in bonelist:
# explanation:
# v * CHILD * PARENT * ...
# = v * CHILD * DIFF^-1 * DIFF * PARENT * ...
# and now choose DIFF such that DIFF * PARENT * ... = desired transform
# calculate desired transform relative to skeleton root
# transform is DIFF * PARENT
transform = (bonedata.get_transform().get_inverse(fast=False)
* geom.get_transform(self))
# calculate difference
diff = transform * bonenode.get_transform(self).get_inverse(fast=False)
if not diff.is_identity():
logger.info("Sending %s to bind position"
% bonenode.name)
# fix transform of this node
bonenode.set_transform(diff * bonenode.get_transform())
# fix transform of all its children
diff_inv = diff.get_inverse(fast=False)
for childnode in bonenode.children:
if childnode:
childnode.set_transform(childnode.get_transform() * diff_inv)
else:
logger.debug("%s is already in bind position"
% bonenode.name)
# validate
error = 0.0
diff_error = 0.0
for geom in geoms:
skininst = geom.skin_instance
skindata = skininst.data
# calculate geometry transform
geomtransform = geom.get_transform(self)
# check skin data fields (also see NiGeometry.update_bind_position)
for i, bone in enumerate(skininst.bones):
diff = ((skindata.bone_list[i].get_transform().get_inverse(fast=False)
* geomtransform)
- bone.get_transform(self))
# calculate error (sup norm)
diff_error = max(max(abs(elem) for elem in row)
for row in diff.as_list())
if diff_error > 1e-3:
logger.warning(
"Failed to set bind position of bone %s for geometry %s (error is %f)"
% (bone.name, geom.name, diff_error))
error = max(error, diff_error)
logger.debug("Bone bind position maximal error is %f" % error)
if error > 1e-3:
logger.warning("Failed to send some bones to bind position")
return error
class NiObjectNET:
def add_extra_data(self, extrablock):
"""Add block to extra data list and extra data chain. It is good practice
to ensure that the extra data has empty next_extra_data field when adding it
to avoid loops in the hierarchy."""
# add to the list
num_extra = self.num_extra_data_list
self.num_extra_data_list = num_extra + 1
self.extra_data_list.update_size()
self.extra_data_list[num_extra] = extrablock
# add to the chain
if not self.extra_data:
self.extra_data = extrablock
else:
lastextra = self.extra_data
while lastextra.next_extra_data:
lastextra = lastextra.next_extra_data
lastextra.next_extra_data = extrablock
def remove_extra_data(self, extrablock):
"""Remove block from extra data list and extra data chain.
>>> from pyffi.formats.nif import NifFormat
>>> block = NifFormat.NiNode()
>>> block.num_extra_data_list = 3
>>> block.extra_data_list.update_size()
>>> extrablock = NifFormat.NiStringExtraData()
>>> block.extra_data_list[1] = extrablock
>>> block.remove_extra_data(extrablock)
>>> [extra for extra in block.extra_data_list]
[None, None]
"""
# remove from list
new_extra_list = []
for extraother in self.extra_data_list:
if not extraother is extrablock:
new_extra_list.append(extraother)
self.num_extra_data_list = len(new_extra_list)
self.extra_data_list.update_size()
for i, extraother in enumerate(new_extra_list):
self.extra_data_list[i] = extraother
# remove from chain
if self.extra_data is extrablock:
self.extra_data = extrablock.next_extra_data
lastextra = self.extra_data
while lastextra:
if lastextra.next_extra_data is extrablock:
lastextra.next_extra_data = lastextra.next_extra_data.next_extra_data
lastextra = lastextra.next_extra_data
def get_extra_datas(self):
"""Get a list of all extra data blocks."""
xtras = [xtra for xtra in self.extra_data_list]
xtra = self.extra_data
while xtra:
if not xtra in self.extra_data_list:
xtras.append(xtra)
xtra = xtra.next_extra_data
return xtras
def set_extra_datas(self, extralist):
"""Set all extra data blocks from given list (erases existing data).
>>> from pyffi.formats.nif import NifFormat
>>> node = NifFormat.NiNode()
>>> extra1 = NifFormat.NiExtraData()
>>> extra1.name = "hello"
>>> extra2 = NifFormat.NiExtraData()
>>> extra2.name = "world"
>>> node.get_extra_datas()
[]
>>> node.set_extra_datas([extra1, extra2])
>>> [extra.name for extra in node.get_extra_datas()]
['hello', 'world']
>>> [extra.name for extra in node.extra_data_list]
['hello', 'world']
>>> node.extra_data is extra1
True
>>> extra1.next_extra_data is extra2
True
>>> extra2.next_extra_data is None
True
>>> node.set_extra_datas([])
>>> node.get_extra_datas()
[]
>>> # now set them the other way around
>>> node.set_extra_datas([extra2, extra1])
>>> [extra.name for extra in node.get_extra_datas()]
['world', 'hello']
>>> [extra.name for extra in node.extra_data_list]
['world', 'hello']
>>> node.extra_data is extra2
True
>>> extra2.next_extra_data is extra1
True
>>> extra1.next_extra_data is None
True
:param extralist: List of extra data blocks to add.
:type extralist: ``list`` of L{NifFormat.NiExtraData}
"""
# set up extra data list
self.num_extra_data_list = len(extralist)
self.extra_data_list.update_size()
for i, extra in enumerate(extralist):
self.extra_data_list[i] = extra
# set up extra data chain
# first, kill the current chain
self.extra_data = None
# now reconstruct it
if extralist:
self.extra_data = extralist[0]
lastextra = self.extra_data
for extra in extralist[1:]:
lastextra.next_extra_data = extra
lastextra = extra
lastextra.next_extra_data = None
def add_controller(self, ctrlblock):
"""Add block to controller chain and set target of controller to self."""
if not self.controller:
self.controller = ctrlblock
else:
lastctrl = self.controller
while lastctrl.next_controller:
lastctrl = lastctrl.next_controller
lastctrl.next_controller = ctrlblock
# set the target of the controller
ctrlblock.target = self
def get_controllers(self):
"""Get a list of all controllers."""
ctrls = []
ctrl = self.controller
while ctrl:
ctrls.append(ctrl)
ctrl = ctrl.next_controller
return ctrls
def add_integer_extra_data(self, name, value):
"""Add a particular extra integer data block."""
extra = NifFormat.NiIntegerExtraData()
extra.name = name
extra.integer_data = value
self.add_extra_data(extra)
class NiObject:
def find(self, block_name = None, block_type = None):
# does this block match the search criteria?
if block_name and block_type:
if isinstance(self, block_type):
try:
if block_name == self.name: return self
except AttributeError:
pass
elif block_name:
try:
if block_name == self.name: return self
except AttributeError:
pass
elif block_type:
if isinstance(self, block_type): return self
# ok, this block is not a match, so check further down in tree
for child in self.get_refs():
blk = child.find(block_name, block_type)
if blk: return blk
return None
def find_chain(self, block, block_type = None):
"""Finds a chain of blocks going from C{self} to C{block}. If found,
self is the first element and block is the last element. If no branch
found, returns an empty list. Does not check whether there is more
than one branch; if so, the first one found is returned.
:param block: The block to find a chain to.
:param block_type: The type that blocks should have in this chain."""
if self is block: return [self]
for child in self.get_refs():
if block_type and not isinstance(child, block_type): continue
child_chain = child.find_chain(block, block_type)
if child_chain:
return [self] + child_chain
return []
def apply_scale(self, scale):
"""Scale data in this block. This implementation does nothing.
Override this method if it contains geometry data that can be
scaled.
"""
pass
def tree(self, block_type = None, follow_all = True, unique = False):
"""A generator for parsing all blocks in the tree (starting from and
including C{self}).
:param block_type: If not ``None``, yield only blocks of the type C{block_type}.
:param follow_all: If C{block_type} is not ``None``, then if this is ``True`` the function will parse the whole tree. Otherwise, the function will not follow branches that start by a non-C{block_type} block.
:param unique: Whether the generator can return the same block twice or not."""
# unique blocks: reduce this to the case of non-unique blocks
if unique:
block_list = []
for block in self.tree(block_type = block_type, follow_all = follow_all, unique = False):
if not block in block_list:
yield block
block_list.append(block)
return
# yield self
if not block_type:
yield self
elif isinstance(self, block_type):
yield self
elif not follow_all:
return # don't recurse further
# yield tree attached to each child
for child in self.get_refs():
for block in child.tree(block_type = block_type, follow_all = follow_all):
yield block
def _validateTree(self):
"""Raises ValueError if there is a cycle in the tree."""
# If the tree is parsed, then each block should be visited once.
# However, as soon as some cycle is present, parsing the tree
# will visit some child more than once (and as a consequence, infinitely
# many times). So, walk the reference tree and check that every block is
# only visited once.
children = []
for child in self.tree():
if child in children:
raise ValueError('cyclic references detected')
children.append(child)
def is_interchangeable(self, other):
"""Are the two blocks interchangeable?
@todo: Rely on AnyType, SimpleType, ComplexType, etc. implementation.
"""
if isinstance(self, (NifFormat.NiProperty, NifFormat.NiSourceTexture)):
# use hash for properties and source textures
return ((self.__class__ is other.__class__)
and (self.get_hash() == other.get_hash()))
else:
# for blocks with references: quick check only
return self is other
class ATextureRenderData:
def save_as_dds(self, stream):
"""Save image as DDS file."""
# set up header and pixel data
data = pyffi.formats.dds.DdsFormat.Data()
header = data.header
pixeldata = data.pixeldata
# create header, depending on the format
if self.pixel_format in (NifFormat.PixelFormat.PX_FMT_RGB8,
NifFormat.PixelFormat.PX_FMT_RGBA8):
# uncompressed RGB(A)
header.flags.caps = 1
header.flags.height = 1
header.flags.width = 1
header.flags.pixel_format = 1
header.flags.mipmap_count = 1
header.flags.linear_size = 1
header.height = self.mipmaps[0].height
header.width = self.mipmaps[0].width
header.linear_size = len(self.pixel_data)
header.mipmap_count = len(self.mipmaps)
header.pixel_format.flags.rgb = 1
header.pixel_format.bit_count = self.bits_per_pixel
header.pixel_format.r_mask = self.red_mask
header.pixel_format.g_mask = self.green_mask
header.pixel_format.b_mask = self.blue_mask
header.pixel_format.a_mask = self.alpha_mask
header.caps_1.complex = 1
header.caps_1.texture = 1
header.caps_1.mipmap = 1
pixeldata.set_value(self.pixel_data)
elif self.pixel_format == NifFormat.PixelFormat.PX_FMT_DXT1:
# format used in Megami Tensei: Imagine
header.flags.caps = 1
header.flags.height = 1
header.flags.width = 1
header.flags.pixel_format = 1
header.flags.mipmap_count = 1
header.flags.linear_size = 0
header.height = self.mipmaps[0].height
header.width = self.mipmaps[0].width
header.linear_size = 0
header.mipmap_count = len(self.mipmaps)
header.pixel_format.flags.fourcc = 1
header.pixel_format.fourcc = pyffi.formats.dds.DdsFormat.FourCC.DXT1
header.pixel_format.bit_count = 0
header.pixel_format.r_mask = 0
header.pixel_format.g_mask = 0
header.pixel_format.b_mask = 0
header.pixel_format.a_mask = 0
header.caps_1.complex = 1
header.caps_1.texture = 1
header.caps_1.mipmap = 1
if isinstance(self,
NifFormat.NiPersistentSrcTextureRendererData):
pixeldata.set_value(
''.join(
''.join([chr(x) for x in tex])
for tex in self.pixel_data))
else:
pixeldata.set_value(''.join(self.pixel_data_matrix))
elif self.pixel_format in (NifFormat.PixelFormat.PX_FMT_DXT5,
NifFormat.PixelFormat.PX_FMT_DXT5_ALT):
# format used in Megami Tensei: Imagine
header.flags.caps = 1
header.flags.height = 1
header.flags.width = 1
header.flags.pixel_format = 1
header.flags.mipmap_count = 1
header.flags.linear_size = 0
header.height = self.mipmaps[0].height
header.width = self.mipmaps[0].width
header.linear_size = 0
header.mipmap_count = len(self.mipmaps)
header.pixel_format.flags.fourcc = 1
header.pixel_format.fourcc = pyffi.formats.dds.DdsFormat.FourCC.DXT5
header.pixel_format.bit_count = 0
header.pixel_format.r_mask = 0
header.pixel_format.g_mask = 0
header.pixel_format.b_mask = 0
header.pixel_format.a_mask = 0
header.caps_1.complex = 1
header.caps_1.texture = 1
header.caps_1.mipmap = 1
pixeldata.set_value(''.join(self.pixel_data_matrix))
else:
raise ValueError(
"cannot save pixel format %i as DDS" % self.pixel_format)
data.write(stream)
class NiSkinData:
def get_transform(self):
"""Return scale, rotation, and translation into a single 4x4 matrix."""
mat = NifFormat.Matrix44()
mat.set_scale_rotation_translation(self.scale, self.rotation, self.translation)
return mat
def set_transform(self, mat):
"""Set rotation, transform, and velocity."""
scale, rotation, translation = mat.get_scale_rotation_translation()
self.scale = scale
self.rotation.m_11 = rotation.m_11
self.rotation.m_12 = rotation.m_12
self.rotation.m_13 = rotation.m_13
self.rotation.m_21 = rotation.m_21
self.rotation.m_22 = rotation.m_22
self.rotation.m_23 = rotation.m_23
self.rotation.m_31 = rotation.m_31
self.rotation.m_32 = rotation.m_32
self.rotation.m_33 = rotation.m_33
self.translation.x = translation.x
self.translation.y = translation.y
self.translation.z = translation.z
def apply_scale(self, scale):
"""Apply scale factor on data.
>>> from pyffi.formats.nif import NifFormat
>>> id44 = NifFormat.Matrix44()
>>> id44.set_identity()
>>> skelroot = NifFormat.NiNode()
>>> skelroot.name = 'Scene Root'
>>> skelroot.set_transform(id44)
>>> bone1 = NifFormat.NiNode()
>>> bone1.name = 'bone1'
>>> bone1.set_transform(id44)
>>> bone1.translation.x = 10
>>> skelroot.add_child(bone1)
>>> geom = NifFormat.NiTriShape()
>>> geom.set_transform(id44)
>>> skelroot.add_child(geom)
>>> skininst = NifFormat.NiSkinInstance()
>>> geom.skin_instance = skininst
>>> skininst.skeleton_root = skelroot
>>> skindata = NifFormat.NiSkinData()
>>> skininst.data = skindata
>>> skindata.set_transform(id44)
>>> geom.add_bone(bone1, {})
>>> geom.update_bind_position()
>>> bone1.translation.x
10.0
>>> skindata.bone_list[0].translation.x
-10.0
>>> import pyffi.spells.nif.fix
>>> import pyffi.spells.nif
>>> data = NifFormat.Data()
>>> data.roots = [skelroot]
>>> toaster = pyffi.spells.nif.NifToaster()
>>> toaster.scale = 0.1
>>> pyffi.spells.nif.fix.SpellScale(data=data, toaster=toaster).recurse()
pyffi.toaster:INFO:--- fix_scale ---
pyffi.toaster:INFO: scaling by factor 0.100000
pyffi.toaster:INFO: ~~~ NiNode [Scene Root] ~~~
pyffi.toaster:INFO: ~~~ NiNode [bone1] ~~~
pyffi.toaster:INFO: ~~~ NiTriShape [] ~~~
pyffi.toaster:INFO: ~~~ NiSkinInstance [] ~~~
pyffi.toaster:INFO: ~~~ NiSkinData [] ~~~
>>> bone1.translation.x
1.0
>>> skindata.bone_list[0].translation.x
-1.0
"""
self.translation.x *= scale
self.translation.y *= scale
self.translation.z *= scale
for skindata in self.bone_list:
skindata.translation.x *= scale
skindata.translation.y *= scale
skindata.translation.z *= scale
skindata.bounding_sphere_offset.x *= scale
skindata.bounding_sphere_offset.y *= scale
skindata.bounding_sphere_offset.z *= scale
skindata.bounding_sphere_radius *= scale
class NiTransformInterpolator:
def apply_scale(self, scale):
"""Apply scale factor <scale> on data."""
# apply scale on translation
self.translation.x *= scale
self.translation.y *= scale
self.translation.z *= scale
class NiTriBasedGeomData:
def is_interchangeable(self, other):
"""Heuristically checks if two NiTriBasedGeomData blocks describe
the same geometry, that is, if they can be used interchangeably in
a nif file without affecting the rendering. The check is not fool
proof but has shown to work in most practical cases.
:param other: Another geometry data block.
:type other: L{NifFormat.NiTriBasedGeomData} (if it has another type
then the function will always return ``False``)
:return: ``True`` if the geometries are equivalent, ``False`` otherwise.
"""
# check for object identity
if self is other:
return True
# type check
if not isinstance(other, NifFormat.NiTriBasedGeomData):
return False
# check class
if (not isinstance(self, other.__class__)
or not isinstance(other, self.__class__)):
return False
# check some trivial things first
for attribute in (
"num_vertices", "keep_flags", "compress_flags", "has_vertices",
"num_uv_sets", "has_normals", "center", "radius",
"has_vertex_colors", "has_uv", "consistency_flags"):
if getattr(self, attribute) != getattr(other, attribute):
return False
# check vertices (this includes uvs, vcols and normals)
verthashes1 = [hsh for hsh in self.get_vertex_hash_generator()]
verthashes2 = [hsh for hsh in other.get_vertex_hash_generator()]
for hash1 in verthashes1:
if not hash1 in verthashes2:
return False
for hash2 in verthashes2:
if not hash2 in verthashes1:
return False
# check triangle list
triangles1 = [tuple(verthashes1[i] for i in tri)
for tri in self.get_triangles()]
triangles2 = [tuple(verthashes2[i] for i in tri)
for tri in other.get_triangles()]
for tri1 in triangles1:
if not tri1 in triangles2:
return False
for tri2 in triangles2:
if not tri2 in triangles1:
return False
# looks pretty identical!
return True
def get_triangle_indices(self, triangles):
"""Yield list of triangle indices (relative to
self.get_triangles()) of given triangles. Degenerate triangles in
the list are assigned index ``None``.
>>> from pyffi.formats.nif import NifFormat
>>> geomdata = NifFormat.NiTriShapeData()
>>> geomdata.set_triangles([(0,1,2),(1,2,3),(2,3,4)])
>>> list(geomdata.get_triangle_indices([(1,2,3)]))
[1]
>>> list(geomdata.get_triangle_indices([(3,1,2)]))
[1]
>>> list(geomdata.get_triangle_indices([(2,3,1)]))
[1]
>>> list(geomdata.get_triangle_indices([(1,2,0),(4,2,3)]))
[0, 2]
>>> list(geomdata.get_triangle_indices([(0,0,0),(4,2,3)]))
[None, 2]
>>> list(geomdata.get_triangle_indices([(0,3,4),(4,2,3)])) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: ...
:param triangles: An iterable of triangles to check.
:type triangles: iterator or list of tuples of three ints
"""
def triangleHash(triangle):
"""Calculate hash of a non-degenerate triangle.
Returns ``None`` if the triangle is degenerate.
"""
if triangle[0] < triangle[1] and triangle[0] < triangle[2]:
return hash((triangle[0], triangle[1], triangle[2]))
elif triangle[1] < triangle[0] and triangle[1] < triangle[2]:
return hash((triangle[1], triangle[2], triangle[0]))
elif triangle[2] < triangle[0] and triangle[2] < triangle[1]:
return hash((triangle[2], triangle[0], triangle[1]))
# calculate hashes of all triangles in the geometry
self_triangles_hashes = [
triangleHash(triangle) for triangle in self.get_triangles()]
# calculate index of each triangle in the list of triangles
for triangle in triangles:
triangle_hash = triangleHash(triangle)
if triangle_hash is None:
yield None
else:
yield self_triangles_hashes.index(triangle_hash)
class NiTriBasedGeom:
def get_tangent_space(self):
"""Return iterator over normal, tangent, bitangent vectors.
If the block has no tangent space, then returns None.
"""
def bytes2vectors(data, pos, num):
for i in xrange(num):
# data[pos:pos+12] is not really well implemented, so do this
vecdata = ''.join(data[j] for j in xrange(pos, pos + 12))
vec = NifFormat.Vector3()
vec.x, vec.y, vec.z = struct.unpack('<fff', vecdata)
yield vec
pos += 12
if self.data.num_vertices == 0:
return ()
if not self.data.normals:
#raise ValueError('geometry has no normals')
return None
if (not self.data.tangents) or (not self.data.bitangents):
# no tangents and bitangents at the usual location
# perhaps there is Oblivion style data?
for extra in self.get_extra_datas():
if isinstance(extra, NifFormat.NiBinaryExtraData):
if extra.name == 'Tangent space (binormal & tangent vectors)':
break
else:
#raise ValueError('geometry has no tangents')
return None
if 24 * self.data.num_vertices != len(extra.binary_data):
raise ValueError(
'tangent space data has invalid size, expected %i bytes but got %i'
% (24 * self.data.num_vertices, len(extra.binary_data)))
tangents = bytes2vectors(extra.binary_data,
0,
self.data.num_vertices)
bitangents = bytes2vectors(extra.binary_data,
12 * self.data.num_vertices,
self.data.num_vertices)
else:
tangents = self.data.tangents
bitangents = self.data.bitangents
return izip(self.data.normals, tangents, bitangents)
def update_tangent_space(self, as_extra=None):
"""Recalculate tangent space data.
:param as_extra: Whether to store the tangent space data as extra data
(as in Oblivion) or not (as in Fallout 3). If not set, switches to
Oblivion if an extra data block is found, otherwise does default.
Set it to override this detection (for example when using this
function to create tangent space data) and force behaviour.
"""
# check that self.data exists and is valid
if not isinstance(self.data, NifFormat.NiTriBasedGeomData):
raise ValueError(
'cannot update tangent space of a geometry with %s data'
%(self.data.__class__ if self.data else 'no'))
verts = self.data.vertices
norms = self.data.normals
if len(self.data.uv_sets) > 0:
uvs = self.data.uv_sets[0]
else:
return # no uv sets so no tangent space
# check that shape has norms and uvs
if len(uvs) == 0 or len(norms) == 0: return
bin = []
tan = []
for i in xrange(self.data.num_vertices):
bin.append(NifFormat.Vector3())
tan.append(NifFormat.Vector3())
# calculate tangents and binormals from vertex and texture coordinates
for t1, t2, t3 in self.data.get_triangles():
# skip degenerate triangles
if t1 == t2 or t2 == t3 or t3 == t1: continue
v_1 = verts[t1]
v_2 = verts[t2]
v_3 = verts[t3]
w1 = uvs[t1]
w2 = uvs[t2]
w3 = uvs[t3]
v_2v_1 = v_2 - v_1
v_3v_1 = v_3 - v_1
w2w1 = w2 - w1
w3w1 = w3 - w1
# surface of triangle in texture space
r = w2w1.u * w3w1.v - w3w1.u * w2w1.v
# sign of surface
r_sign = (1 if r >= 0 else -1)
# contribution of this triangle to tangents and binormals
sdir = NifFormat.Vector3()
sdir.x = w3w1.v * v_2v_1.x - w2w1.v * v_3v_1.x
sdir.y = w3w1.v * v_2v_1.y - w2w1.v * v_3v_1.y
sdir.z = w3w1.v * v_2v_1.z - w2w1.v * v_3v_1.z
sdir *= r_sign
try:
sdir.normalize()
except ZeroDivisionError: # catches zero vector
continue # skip triangle
except ValueError: # catches invalid data
continue # skip triangle
tdir = NifFormat.Vector3()
tdir.x = w2w1.u * v_3v_1.x - w3w1.u * v_2v_1.x
tdir.y = w2w1.u * v_3v_1.y - w3w1.u * v_2v_1.y
tdir.z = w2w1.u * v_3v_1.z - w3w1.u * v_2v_1.z
tdir *= r_sign
try:
tdir.normalize()
except ZeroDivisionError: # catches zero vector
continue # skip triangle
except ValueError: # catches invalid data
continue # skip triangle
# vector combination algorithm could possibly be improved
for i in [t1, t2, t3]:
tan[i] += tdir
bin[i] += sdir
xvec = NifFormat.Vector3()
xvec.x = 1.0
xvec.y = 0.0
xvec.z = 0.0
yvec = NifFormat.Vector3()
yvec.x = 0.0
yvec.y = 1.0
yvec.z = 0.0
for i in xrange(self.data.num_vertices):
n = norms[i]
try:
n.normalize()
except (ValueError, ZeroDivisionError):
# this happens if the normal has NAN values or is zero
# just pick something in that case
n = yvec
try:
# turn n, bin, tan into a base via Gram-Schmidt
bin[i] -= n * (n * bin[i])
bin[i].normalize()
tan[i] -= n * (n * tan[i])
tan[i] -= bin[i] * (bin[i] * tan[i])
tan[i].normalize()
except ZeroDivisionError:
# insuffient data to set tangent space for this vertex
# in that case pick a space
bin[i] = xvec.crossproduct(n)
try:
bin[i].normalize()
except ZeroDivisionError:
bin[i] = yvec.crossproduct(n)
bin[i].normalize() # should work now
tan[i] = n.crossproduct(bin[i])
# find possible extra data block
for extra in self.get_extra_datas():
if isinstance(extra, NifFormat.NiBinaryExtraData):
if extra.name == 'Tangent space (binormal & tangent vectors)':
break
else:
extra = None
# if autodetection is on, do as_extra only if an extra data block is found
if as_extra is None:
if extra:
as_extra = True
else:
as_extra = False
if as_extra:
# if tangent space extra data already exists, use it
if not extra:
# otherwise, create a new block and link it
extra = NifFormat.NiBinaryExtraData()
extra.name = 'Tangent space (binormal & tangent vectors)'
self.add_extra_data(extra)
# write the data
binarydata = ""
for vec in tan + bin:
binarydata += struct.pack('<fff', vec.x, vec.y, vec.z)
extra.binary_data = binarydata
else:
# set tangent space flag
# XXX used to be 61440
# XXX from Sid Meier's Railroad & Fallout 3 nifs, 4096 is
# XXX sufficient?
self.data.num_uv_sets |= 4096
self.data.bs_num_uv_sets |= 4096
self.data.tangents.update_size()
self.data.bitangents.update_size()
for vec, data_tan in izip(tan, self.data.tangents):
data_tan.x = vec.x
data_tan.y = vec.y
data_tan.z = vec.z
for vec, data_bitan in izip(bin, self.data.bitangents):
data_bitan.x = vec.x
data_bitan.y = vec.y
data_bitan.z = vec.z
# ported from nifskope/skeleton.cpp:spSkinPartition
def update_skin_partition(self,
maxbonesperpartition=4, maxbonespervertex=4,
verbose=0, stripify=True, stitchstrips=False,
padbones=False,
triangles=None, trianglepartmap=None,
maximize_bone_sharing=False):
"""Recalculate skin partition data.
:deprecated: Do not use the verbose argument.
:param maxbonesperpartition: Maximum number of bones in each partition.
The num_bones field will not exceed this number.
:param maxbonespervertex: Maximum number of bones per vertex.
The num_weights_per_vertex field will be exactly equal to this number.
:param verbose: Ignored, and deprecated. Set pyffi's log level instead.
:param stripify: If true, stripify the partitions, otherwise use triangles.
:param stitchstrips: If stripify is true, then set this to true to stitch
the strips.
:param padbones: Enforces the numbones field to be equal to
maxbonesperpartition. Also ensures that the bone indices are unique
and sorted, per vertex. Raises an exception if maxbonespervertex
is not equal to maxbonesperpartition (in that case bone indices cannot
be unique and sorted). This options is required for Freedom Force vs.
the 3rd Reich skin partitions.
:param triangles: The triangles of the partition (if not specified, then
this defaults to C{self.data.get_triangles()}.
:param trianglepartmap: Maps each triangle to a partition index. Faces with
different indices will never appear in the same partition. If the skin
instance is a BSDismemberSkinInstance, then these indices are used as
body part types, and the partitions in the BSDismemberSkinInstance are
updated accordingly. Note that the faces are counted relative to
L{triangles}.
:param maximize_bone_sharing: Maximize bone sharing between partitions.
This option is useful for Fallout 3.
"""
logger = logging.getLogger("pyffi.nif.nitribasedgeom")
# if trianglepartmap not specified, map everything to index 0
if trianglepartmap is None:
trianglepartmap = repeat(0)
# shortcuts relevant blocks
if not self.skin_instance:
# no skin, nothing to do
return
self._validateSkin()
geomdata = self.data
skininst = self.skin_instance
skindata = skininst.data
# get skindata vertex weights
logger.debug("Getting vertex weights.")
weights = self.get_vertex_weights()
# count minimum and maximum number of bones per vertex
minbones = min(len(weight) for weight in weights)
maxbones = max(len(weight) for weight in weights)
if minbones <= 0:
noweights = [v for v, weight in enumerate(weights)
if not weight]
#raise ValueError(
logger.warn(
'bad NiSkinData: some vertices have no weights %s'
% noweights)
logger.info("Counted minimum of %i and maximum of %i bones per vertex"
% (minbones, maxbones))
# reduce bone influences to meet maximum number of bones per vertex
logger.info("Imposing maximum of %i bones per vertex." % maxbonespervertex)
lostweight = 0.0
for weight in weights:
if len(weight) > maxbonespervertex:
# delete bone influences with least weight
weight.sort(key=lambda x: x[1], reverse=True) # sort by weight
# save lost weight to return to user
lostweight = max(
lostweight, max(
[x[1] for x in weight[maxbonespervertex:]]))
del weight[maxbonespervertex:] # only keep first elements
# normalize
totalweight = sum([x[1] for x in weight]) # sum of all weights
for x in weight: x[1] /= totalweight
maxbones = maxbonespervertex
# sort by again by bone (relied on later when matching vertices)
weight.sort(key=lambda x: x[0])
# reduce bone influences to meet maximum number of bones per partition
# (i.e. maximum number of bones per triangle)
logger.info(
"Imposing maximum of %i bones per triangle (and hence, per partition)."
% maxbonesperpartition)
if triangles is None:
triangles = geomdata.get_triangles()
for tri in triangles:
while True:
# find the bones influencing this triangle
tribones = []
for t in tri:
tribones.extend([bonenum for bonenum, boneweight in weights[t]])
tribones = set(tribones)
# target met?
if len(tribones) <= maxbonesperpartition:
break
# no, need to remove a bone
# sum weights for each bone to find the one that least influences
# this triangle
tribonesweights = {}
for bonenum in tribones: tribonesweights[bonenum] = 0.0
nono = set() # bones with weight 1 cannot be removed
for skinweights in [weights[t] for t in tri]:
# skinweights[0] is the first skinweight influencing vertex t
# and skinweights[0][0] is the bone number of that bone
if len(skinweights) == 1: nono.add(skinweights[0][0])
for bonenum, boneweight in skinweights:
tribonesweights[bonenum] += boneweight
# select a bone to remove
# first find bones we can remove
# restrict to bones not in the nono set
tribonesweights = [
x for x in tribonesweights.items() if x[0] not in nono]
if not tribonesweights:
raise ValueError(
"cannot remove anymore bones in this skin; "
"increase maxbonesperpartition and try again")
# sort by vertex weight sum the last element of this list is now a
# candidate for removal
tribonesweights.sort(key=lambda x: x[1], reverse=True)
minbone = tribonesweights[-1][0]
# remove minbone from all vertices of this triangle and from all
# matching vertices
for t in tri:
for tt in [t]: #match[t]:
# remove bone
weight = weights[tt]
for i, (bonenum, boneweight) in enumerate(weight):
if bonenum == minbone:
# save lost weight to return to user
lostweight = max(lostweight, boneweight)
del weight[i]
break
else:
continue
# normalize
totalweight = sum([x[1] for x in weight])
for x in weight:
x[1] /= totalweight
# split triangles into partitions
logger.info("Creating partitions")
parts = []
# keep creating partitions as long as there are triangles left
while triangles:
# create a partition
part = [set(), [], None] # bones, triangles, partition index
usedverts = set()
addtriangles = True
# keep adding triangles to it as long as the flag is set
while addtriangles:
# newtriangles is a list of triangles that have not been added to
# the partition, similar for newtrianglepartmap
newtriangles = []
newtrianglepartmap = []
for tri, partindex in izip(triangles, trianglepartmap):
# find the bones influencing this triangle
tribones = []
for t in tri:
tribones.extend([
bonenum for bonenum, boneweight in weights[t]])
tribones = set(tribones)
# if part has no bones,
# or if part has all bones of tribones and index coincides
# then add this triangle to this part
if ((not part[0])
or ((part[0] >= tribones) and (part[2] == partindex))):
part[0] |= tribones
part[1].append(tri)
usedverts |= set(tri)
# if part was empty, assign it the index
if part[2] is None:
part[2] = partindex
else:
newtriangles.append(tri)
newtrianglepartmap.append(partindex)
triangles = newtriangles
trianglepartmap = newtrianglepartmap
# if we have room left in the partition
# then add adjacent triangles
addtriangles = False
newtriangles = []
newtrianglepartmap = []
if len(part[0]) < maxbonesperpartition:
for tri, partindex in izip(triangles, trianglepartmap):
# if triangle is adjacent, and has same index
# then check if it can be added to the partition
if (usedverts & set(tri)) and (part[2] == partindex):
# find the bones influencing this triangle
tribones = []
for t in tri:
tribones.extend([
bonenum for bonenum, boneweight in weights[t]])
tribones = set(tribones)
# and check if we exceed the maximum number of allowed
# bones
if len(part[0] | tribones) <= maxbonesperpartition:
part[0] |= tribones
part[1].append(tri)
usedverts |= set(tri)
# signal another try in adding triangles to
# the partition
addtriangles = True
else:
newtriangles.append(tri)
newtrianglepartmap.append(partindex)
else:
newtriangles.append(tri)
newtrianglepartmap.append(partindex)
triangles = newtriangles
trianglepartmap = newtrianglepartmap
parts.append(part)
logger.info("Created %i small partitions." % len(parts))
# merge all partitions
logger.info("Merging partitions.")
merged = True # signals success, in which case do another run
while merged:
merged = False
# newparts is to contain the updated merged partitions as we go
newparts = []
# addedparts is the set of all partitions from parts that have been
# added to newparts
addedparts = set()
# try all combinations
for a, parta in enumerate(parts):
if a in addedparts:
continue
newparts.append(parta)
addedparts.add(a)
for b, partb in enumerate(parts):
if b <= a:
continue
if b in addedparts:
continue
# if partition indices are the same, and bone limit is not
# exceeded, merge them
if ((parta[2] == partb[2])
and (len(parta[0] | partb[0]) <= maxbonesperpartition)):
parta[0] |= partb[0]
parta[1] += partb[1]
addedparts.add(b)
merged = True # signal another try in merging partitions
# update partitions to the merged partitions
parts = newparts
# write the NiSkinPartition
logger.info("Skin has %i partitions." % len(parts))
# if skin partition already exists, use it
if skindata.skin_partition != None:
skinpart = skindata.skin_partition
skininst.skin_partition = skinpart
elif skininst.skin_partition != None:
skinpart = skininst.skin_partition
skindata.skin_partition = skinpart
else:
# otherwise, create a new block and link it
skinpart = NifFormat.NiSkinPartition()
skindata.skin_partition = skinpart
skininst.skin_partition = skinpart
# set number of partitions
skinpart.num_skin_partition_blocks = len(parts)
skinpart.skin_partition_blocks.update_size()
# maximize bone sharing, if requested
if maximize_bone_sharing:
logger.info("Maximizing shared bones.")
# new list of partitions, sorted to maximize bone sharing
newparts = []
# as long as there are parts to add
while parts:
# current set of partitions with shared bones
# starts a new set of partitions with shared bones
sharedparts = [parts.pop()]
sharedboneset = sharedparts[0][0]
# go over all other partitions, and try to add them with
# shared bones
oldparts = parts[:]
parts = []
for otherpart in oldparts:
# check if bones can be added
if len(sharedboneset | otherpart[0]) <= maxbonesperpartition:
# ok, we can share bones!
# update set of shared bones
sharedboneset |= otherpart[0]
# add this other partition to list of shared parts
sharedparts.append(otherpart)
# update bone set in all shared parts
for sharedpart in sharedparts:
sharedpart[0] = sharedboneset
else:
# not added to sharedparts,
# so we must keep it for the next iteration
parts.append(otherpart)
# update list of partitions
newparts.extend(sharedparts)
# store update
parts = newparts
# for Fallout 3, set dismember partition indices
if isinstance(skininst, NifFormat.BSDismemberSkinInstance):
skininst.num_partitions = len(parts)
skininst.partitions.update_size()
lastpart = None
for bodypart, part in izip(skininst.partitions, parts):
bodypart.body_part = part[2]
if (lastpart is None) or (lastpart[0] != part[0]):
# start new bone set, if bones are not shared
bodypart.part_flag.start_new_boneset = 1
else:
# do not start new bone set
bodypart.part_flag.start_new_boneset = 0
# caps are invisible
bodypart.part_flag.editor_visible = (part[2] < 100
or part[2] >= 1000)
# store part for next iteration
lastpart = part
for skinpartblock, part in zip(skinpart.skin_partition_blocks, parts):
# get sorted list of bones
bones = sorted(list(part[0]))
triangles = part[1]
# get sorted list of vertices
vertices = set()
for tri in triangles:
vertices |= set(tri)
vertices = sorted(list(vertices))
# remap the vertices
parttriangles = []
for tri in triangles:
parttriangles.append([vertices.index(t) for t in tri])
if stripify:
# stripify the triangles
logger.info("Stripifying partition %i" % parts.index(part))
strips = pyffi.utils.tristrip.stripify(
parttriangles, stitchstrips=stitchstrips)
numtriangles = 0
for strip in strips:
numtriangles += len(strip) - 2
else:
numtriangles = len(parttriangles)
# set all the data
skinpartblock.num_vertices = len(vertices)
skinpartblock.num_triangles = numtriangles
if not padbones:
skinpartblock.num_bones = len(bones)
else:
if maxbonesperpartition != maxbonespervertex:
raise ValueError(
"when padding bones maxbonesperpartition must be "
"equal to maxbonespervertex")
# freedom force vs. the 3rd reich needs exactly 4 bones per
# partition on every partition block
skinpartblock.num_bones = maxbonesperpartition
if stripify:
skinpartblock.num_strips = len(strips)
else:
skinpartblock.num_strips = 0
# maxbones would be enough as num_weights_per_vertex but the Gamebryo
# engine doesn't like that, it seems to want exactly 4 even if there
# are fewer
skinpartblock.num_weights_per_vertex = maxbonespervertex
skinpartblock.bones.update_size()
for i, bonenum in enumerate(bones):
skinpartblock.bones[i] = bonenum
for i in xrange(len(bones), skinpartblock.num_bones):
skinpartblock.bones[i] = 0 # dummy bone slots refer to first bone
skinpartblock.has_vertex_map = True
skinpartblock.vertex_map.update_size()
for i, v in enumerate(vertices):
skinpartblock.vertex_map[i] = v
skinpartblock.has_vertex_weights = True
skinpartblock.vertex_weights.update_size()
for i, v in enumerate(vertices):
for j in xrange(skinpartblock.num_weights_per_vertex):
if j < len(weights[v]):
skinpartblock.vertex_weights[i][j] = weights[v][j][1]
else:
skinpartblock.vertex_weights[i][j] = 0.0
if stripify:
skinpartblock.has_faces = True
skinpartblock.strip_lengths.update_size()
for i, strip in enumerate(strips):
skinpartblock.strip_lengths[i] = len(strip)
skinpartblock.strips.update_size()
for i, strip in enumerate(strips):
for j, v in enumerate(strip):
skinpartblock.strips[i][j] = v
else:
skinpartblock.has_faces = True
# clear strip lengths array
skinpartblock.strip_lengths.update_size()
# clear strips array
skinpartblock.strips.update_size()
skinpartblock.triangles.update_size()
for i, (v_1,v_2,v_3) in enumerate(parttriangles):
skinpartblock.triangles[i].v_1 = v_1
skinpartblock.triangles[i].v_2 = v_2
skinpartblock.triangles[i].v_3 = v_3
skinpartblock.has_bone_indices = True
skinpartblock.bone_indices.update_size()
for i, v in enumerate(vertices):
# the boneindices set keeps track of indices that have not been
# used yet
boneindices = set(range(skinpartblock.num_bones))
for j in xrange(len(weights[v])):
skinpartblock.bone_indices[i][j] = bones.index(weights[v][j][0])
boneindices.remove(skinpartblock.bone_indices[i][j])
for j in xrange(len(weights[v]),skinpartblock.num_weights_per_vertex):
if padbones:
# if padbones is True then we have enforced
# num_bones == num_weights_per_vertex so this will not trigger
# a KeyError
skinpartblock.bone_indices[i][j] = boneindices.pop()
else:
skinpartblock.bone_indices[i][j] = 0
# sort weights
for i, v in enumerate(vertices):
vweights = []
for j in xrange(skinpartblock.num_weights_per_vertex):
vweights.append([
skinpartblock.bone_indices[i][j],
skinpartblock.vertex_weights[i][j]])
if padbones:
# by bone index (for ffvt3r)
vweights.sort(key=lambda w: w[0])
else:
# by weight (for fallout 3, largest weight first)
vweights.sort(key=lambda w: -w[1])
for j in xrange(skinpartblock.num_weights_per_vertex):
skinpartblock.bone_indices[i][j] = vweights[j][0]
skinpartblock.vertex_weights[i][j] = vweights[j][1]
return lostweight
# ported from nifskope/skeleton.cpp:spFixBoneBounds
def update_skin_center_radius(self):
"""Update centers and radii of all skin data fields."""
# shortcuts relevant blocks
if not self.skin_instance:
return # no skin, nothing to do
self._validateSkin()
geomdata = self.data
skininst = self.skin_instance
skindata = skininst.data
verts = geomdata.vertices
for skindatablock in skindata.bone_list:
# find all vertices influenced by this bone
boneverts = [verts[skinweight.index]
for skinweight in skindatablock.vertex_weights]
# find bounding box of these vertices
low = NifFormat.Vector3()
low.x = min(v.x for v in boneverts)
low.y = min(v.y for v in boneverts)
low.z = min(v.z for v in boneverts)
high = NifFormat.Vector3()
high.x = max(v.x for v in boneverts)
high.y = max(v.y for v in boneverts)
high.z = max(v.z for v in boneverts)
# center is in the center of the bounding box
center = (low + high) * 0.5
# radius is the largest distance from the center
r2 = 0.0
for v in boneverts:
d = center - v
r2 = max(r2, d.x*d.x+d.y*d.y+d.z*d.z)
radius = r2 ** 0.5
# transform center in proper coordinates (radius remains unaffected)
center *= skindatablock.get_transform()
# save data
skindatablock.bounding_sphere_offset.x = center.x
skindatablock.bounding_sphere_offset.y = center.y
skindatablock.bounding_sphere_offset.z = center.z
skindatablock.bounding_sphere_radius = radius
def get_interchangeable_tri_shape(self):
"""Returns a NiTriShape block that is geometrically interchangeable."""
# copy the shape (first to NiTriBasedGeom and then to NiTriShape)
shape = NifFormat.NiTriShape().deepcopy(
NifFormat.NiTriBasedGeom().deepcopy(self))
# copy the geometry without strips
shapedata = NifFormat.NiTriShapeData().deepcopy(
NifFormat.NiTriBasedGeomData().deepcopy(self.data))
# update the shape data
shapedata.set_triangles(self.data.get_triangles())
# relink the shape data
shape.data = shapedata
# and return the result
return shape
def get_interchangeable_tri_strips(self):
"""Returns a NiTriStrips block that is geometrically interchangeable."""
# copy the shape (first to NiTriBasedGeom and then to NiTriStrips)
strips = NifFormat.NiTriStrips().deepcopy(
NifFormat.NiTriBasedGeom().deepcopy(self))
# copy the geometry without triangles
stripsdata = NifFormat.NiTriStripsData().deepcopy(
NifFormat.NiTriBasedGeomData().deepcopy(self.data))
# update the shape data
stripsdata.set_triangles(self.data.get_triangles())
# relink the shape data
strips.data = stripsdata
# and return the result
return strips
class NiTriShapeData:
"""
Example usage:
>>> from pyffi.formats.nif import NifFormat
>>> block = NifFormat.NiTriShapeData()
>>> block.set_triangles([(0,1,2),(2,1,3),(2,3,4)])
>>> block.get_strips()
[[0, 1, 2, 3, 4]]
>>> block.get_triangles()
[(0, 1, 2), (2, 1, 3), (2, 3, 4)]
>>> block.set_strips([[1,0,1,2,3,4]])
>>> block.get_strips()
[[4, 3, 2, 1, 0]]
>>> block.get_triangles()
[(0, 2, 1), (1, 2, 3), (2, 4, 3)]
"""
def get_triangles(self):
return [(t.v_1, t.v_2, t.v_3) for t in self.triangles]
def set_triangles(self, triangles, stitchstrips = False):
# note: the stitchstrips argument is ignored - only present to ensure
# uniform interface between NiTriShapeData and NiTriStripsData
# initialize triangle array
n = len(triangles)
self.num_triangles = n
self.num_triangle_points = 3*n
self.has_triangles = (n > 0)
self.triangles.update_size()
# copy triangles
src = triangles.__iter__()
dst = self.triangles.__iter__()
for k in xrange(n):
dst_t = dst.next()
dst_t.v_1, dst_t.v_2, dst_t.v_3 = src.next()
def get_strips(self):
return pyffi.utils.tristrip.stripify(self.get_triangles())
def set_strips(self, strips):
self.set_triangles(pyffi.utils.tristrip.triangulate(strips))
class NiTriStripsData:
"""
Example usage:
>>> from pyffi.formats.nif import NifFormat
>>> block = NifFormat.NiTriStripsData()
>>> block.set_triangles([(0,1,2),(2,1,3),(2,3,4)])
>>> block.get_strips()
[[0, 1, 2, 3, 4]]
>>> block.get_triangles()
[(0, 1, 2), (1, 3, 2), (2, 3, 4)]
>>> block.set_strips([[1,0,1,2,3,4]])
>>> block.get_strips()
[[1, 0, 1, 2, 3, 4]]
>>> block.get_triangles()
[(0, 2, 1), (1, 2, 3), (2, 4, 3)]
"""
def get_triangles(self):
return pyffi.utils.tristrip.triangulate(self.points)
def set_triangles(self, triangles, stitchstrips = False):
self.set_strips(pyffi.utils.tristrip.stripify(triangles, stitchstrips = stitchstrips))
def get_strips(self):
return [[i for i in strip] for strip in self.points]
def set_strips(self, strips):
# initialize strips array
self.num_strips = len(strips)
self.strip_lengths.update_size()
numtriangles = 0
for i, strip in enumerate(strips):
self.strip_lengths[i] = len(strip)
numtriangles += len(strip) - 2
self.num_triangles = numtriangles
self.points.update_size()
self.has_points = (len(strips) > 0)
# copy strips
for i, strip in enumerate(strips):
for j, idx in enumerate(strip):
self.points[i][j] = idx
class RagdollDescriptor:
def update_a_b(self, transform):
"""Update B pivot and axes from A using the given transform."""
# pivot point
pivot_b = ((7 * self.pivot_a.get_vector_3()) * transform) / 7.0
self.pivot_b.x = pivot_b.x
self.pivot_b.y = pivot_b.y
self.pivot_b.z = pivot_b.z
# axes (rotation only)
transform = transform.get_matrix_33()
plane_b = self.plane_a.get_vector_3() * transform
twist_b = self.twist_a.get_vector_3() * transform
self.plane_b.x = plane_b.x
self.plane_b.y = plane_b.y
self.plane_b.z = plane_b.z
self.twist_b.x = twist_b.x
self.twist_b.y = twist_b.y
self.twist_b.z = twist_b.z
class SkinData:
def get_transform(self):
"""Return scale, rotation, and translation into a single 4x4 matrix."""
m = NifFormat.Matrix44()
m.set_scale_rotation_translation(self.scale, self.rotation, self.translation)
return m
def set_transform(self, m):
"""Set rotation, transform, and velocity."""
scale, rotation, translation = m.get_scale_rotation_translation()
self.scale = scale
self.rotation.m_11 = rotation.m_11
self.rotation.m_12 = rotation.m_12
self.rotation.m_13 = rotation.m_13
self.rotation.m_21 = rotation.m_21
self.rotation.m_22 = rotation.m_22
self.rotation.m_23 = rotation.m_23
self.rotation.m_31 = rotation.m_31
self.rotation.m_32 = rotation.m_32
self.rotation.m_33 = rotation.m_33
self.translation.x = translation.x
self.translation.y = translation.y
self.translation.z = translation.z
class StringPalette:
def get_string(self, offset):
"""Return string at given offset.
>>> from pyffi.formats.nif import NifFormat
>>> pal = NifFormat.StringPalette()
>>> pal.add_string("abc")
0
>>> pal.add_string("def")
4
>>> print(pal.get_string(0).decode("ascii"))
abc
>>> print(pal.get_string(4).decode("ascii"))
def
>>> pal.get_string(5) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: ...
>>> pal.get_string(100) # doctest: +ELLIPSIS
Traceback (most recent call last):
...
ValueError: ...
"""
_b00 = pyffi.object_models.common._b00 # shortcut
# check that offset isn't too large
if offset >= len(self.palette):
raise ValueError(
"StringPalette: getting string at %i "
"but palette is only %i long"
% (offset, len(self.palette)))
# check that a string starts at this offset
if offset > 0 and self.palette[offset-1:offset] != _b00:
raise ValueError(
"StringPalette: no string starts at offset %i "
"(palette is %s)" % (offset, self.palette))
# return the string
return self.palette[offset:self.palette.find(_b00, offset)]
def get_all_strings(self):
"""Return a list of all strings.
>>> from pyffi.formats.nif import NifFormat
>>> pal = NifFormat.StringPalette()
>>> pal.add_string("abc")
0
>>> pal.add_string("def")
4
>>> for x in pal.get_all_strings():
... print(x.decode("ascii"))
abc
def
>>> # pal.palette.decode("ascii") needs lstrip magic for py3k
>>> print(repr(pal.palette.decode("ascii")).lstrip("u"))
'abc\\x00def\\x00'
"""
_b00 = pyffi.object_models.common._b00 # shortcut
return self.palette[:-1].split(_b00)
def add_string(self, text):
"""Adds string to palette (will recycle existing strings if possible) and
return offset to the string in the palette.
>>> from pyffi.formats.nif import NifFormat
>>> pal = NifFormat.StringPalette()
>>> pal.add_string("abc")
0
>>> pal.add_string("abc")
0
>>> pal.add_string("def")
4
>>> pal.add_string("")
-1
>>> print(pal.get_string(4).decode("ascii"))
def
"""
# empty text
if not text:
return -1
_b00 = pyffi.object_models.common._b00 # shortcut
# convert text to bytes if necessary
text = pyffi.object_models.common._as_bytes(text)
# check if string is already in the palette
# ... at the start
if text + _b00 == self.palette[:len(text) + 1]:
return 0
# ... or elsewhere
offset = self.palette.find(_b00 + text + _b00)
if offset != -1:
return offset + 1
# if no match, add the string
if offset == -1:
offset = len(self.palette)
self.palette = self.palette + text + _b00
self.length += len(text) + 1
# return the offset
return offset
def clear(self):
"""Clear all strings in the palette.
>>> from pyffi.formats.nif import NifFormat
>>> pal = NifFormat.StringPalette()
>>> pal.add_string("abc")
0
>>> pal.add_string("def")
4
>>> # pal.palette.decode("ascii") needs lstrip magic for py3k
>>> print(repr(pal.palette.decode("ascii")).lstrip("u"))
'abc\\x00def\\x00'
>>> pal.clear()
>>> # pal.palette.decode("ascii") needs lstrip magic for py3k
>>> print(repr(pal.palette.decode("ascii")).lstrip("u"))
''
"""
self.palette = pyffi.object_models.common._b # empty bytes object
self.length = 0
class TexCoord:
def as_list(self):
return [self.u, self.v]
def normalize(self):
r = (self.u*self.u + self.v*self.v) ** 0.5
if r < NifFormat.EPSILON:
raise ZeroDivisionError('cannot normalize vector %s'%self)
self.u /= r
self.v /= r
def __str__(self):
return "[ %6.3f %6.3f ]"%(self.u, self.v)
def __mul__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.TexCoord()
v.u = self.u * x
v.v = self.v * x
return v
elif isinstance(x, NifFormat.TexCoord):
return self.u * x.u + self.v * x.v
else:
raise TypeError("do not know how to multiply TexCoord with %s"%x.__class__)
def __rmul__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.TexCoord()
v.u = x * self.u
v.v = x * self.v
return v
else:
raise TypeError("do not know how to multiply %s and TexCoord"%x.__class__)
def __add__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.TexCoord()
v.u = self.u + x
v.v = self.v + x
return v
elif isinstance(x, NifFormat.TexCoord):
v = NifFormat.TexCoord()
v.u = self.u + x.u
v.v = self.v + x.v
return v
else:
raise TypeError("do not know how to add TexCoord and %s"%x.__class__)
def __radd__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.TexCoord()
v.u = x + self.u
v.v = x + self.v
return v
else:
raise TypeError("do not know how to add %s and TexCoord"%x.__class__)
def __sub__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.TexCoord()
v.u = self.u - x
v.v = self.v - x
return v
elif isinstance(x, NifFormat.TexCoord):
v = NifFormat.TexCoord()
v.u = self.u - x.u
v.v = self.v - x.v
return v
else:
raise TypeError("do not know how to substract TexCoord and %s"%x.__class__)
def __rsub__(self, x):
if isinstance(x, (float, int, long)):
v = NifFormat.TexCoord()
v.u = x - self.u
v.v = x - self.v
return v
else:
raise TypeError("do not know how to substract %s and TexCoord"%x.__class__)
def __neg__(self):
v = NifFormat.TexCoord()
v.u = -self.u
v.v = -self.v
return v
|