diff --git a/btypes/__init__.py b/btypes/__init__.py
new file mode 100644
index 0000000..bc78e3b
--- /dev/null
+++ b/btypes/__init__.py
@@ -0,0 +1,19 @@
+"""Module for reading and writing data structures."""
+
+from btypes.types import *
+
+class FormatError(Exception): pass
+
+SEEK_POS = 0
+SEEK_CUR = 1
+SEEK_END = 2
+
+cstring = CString('ascii')
+pstring = PString('ascii')
+
+
+def align(stream,length,padding=b'This is padding data to alignment.'):
+ if stream.tell() % length == 0: return
+ n,r = divmod(length - (stream.tell() % length),len(padding))
+ stream.write(n*padding + padding[0:r])
+
diff --git a/btypes/big_endian.py b/btypes/big_endian.py
new file mode 100644
index 0000000..66b7269
--- /dev/null
+++ b/btypes/big_endian.py
@@ -0,0 +1,14 @@
+from btypes import *
+
+bool8 = BasicType('>?')
+sint8 = BasicType('>b')
+uint8 = BasicType('>B')
+sint16 = BasicType('>h')
+uint16 = BasicType('>H')
+sint32 = BasicType('>l')
+uint32 = BasicType('>L')
+sint64 = BasicType('>q')
+uint64 = BasicType('>Q')
+float32 = BasicType('>f')
+float64 = BasicType('>d')
+
diff --git a/btypes/types.py b/btypes/types.py
new file mode 100644
index 0000000..b0ae8a9
--- /dev/null
+++ b/btypes/types.py
@@ -0,0 +1,294 @@
+import copy
+from struct import Struct as _Struct
+
+
+class BasicType:
+
+ def __init__(self,format_string):
+ self._struct = _Struct(format_string)
+
+ def pack(self,stream,value):
+ stream.write(self._struct.pack(value))
+
+ def pack_into(self,buffer,offset,value):
+ self._struct.pack_into(buffer,offset,value)
+
+ def unpack(self,stream):
+ return self._struct.unpack(stream.read(self.sizeof()))[0]
+
+ def unpack_from(self,buffer,offset):
+ return self._struct.unpack_from(buffer,offset)[0]
+
+ def sizeof(self):
+ return self._struct.size
+
+
+class FixedPointConverter:
+
+ def __init__(self,integer_type,scale):
+ self.integer_type = integer_type
+ self.scale = scale
+
+ def pack(self,stream,value):
+ self.integer_type.pack(stream,round(value/self.scale))
+
+ def unpack(self,stream):
+ return self.integer_type.unpack(stream)*self.scale
+
+ def sizeof(self):
+ return self.integer_type.sizeof()
+
+
+class EnumConverter:
+
+ def __init__(self,integer_type,enumeration):
+ self.integer_type = integer_type
+ self.enumeration = enumeration
+
+ def pack(self,stream,member):
+ self.integer_type.pack(stream,member.value)
+
+ def unpack(self,stream):
+ return self.enumeration(self.integer_type.unpack(stream))
+
+ def sizeof(self):
+ return self.integer_type.sizeof()
+
+
+class NoneableConverter:
+
+ def __init__(self,base_type,none_value):
+ self.base_type = base_type
+ self.none_value = none_value
+
+ def pack(self,stream,value):
+ self.base_type.pack(stream,value if value is not None else self.none_value)
+
+ def unpack(self,stream):
+ value = self.base_type.unpack(stream)
+ return value if value != self.none_value else None
+
+ def sizeof(self):
+ return self.base_type.sizeof()
+
+
+class ByteString:
+
+ def __init__(self,length):
+ self.length = length
+
+ def pack(self,stream,string):
+ if len(string) != self.length:
+ raise ValueError('expected string of length {}, got string of length {}'.format(self.length,len(string)))
+ stream.write(string)
+
+ def unpack(self,stream):
+ return stream.read(self.length)
+
+ def sizeof(self):
+ return self.length
+
+
+class Array:
+
+ def __init__(self,element_type,length):
+ self.element_type = element_type
+ self.length = length
+
+ def pack(self,stream,array):
+ if len(array) != self.length:
+ raise ValueError('expected array of length {}, got array of length {}'.format(self.length,len(array)))
+ for value in array:
+ self.element_type.pack(stream,value)
+
+ def unpack(self,stream):
+ return [self.element_type.unpack(stream) for _ in range(self.length)]
+
+ def sizeof(self):
+ return self.length*self.element_type.sizeof()
+
+
+class CString:
+
+ def __init__(self,encoding):
+ self.encoding = encoding
+
+ def pack(self,stream,string):
+ stream.write((string + '\0').encode(self.encoding))
+
+ def unpack(self,stream):
+ #XXX: This might not work for all encodings
+ null = '\0'.encode(self.encoding)
+ string = b''
+ while True:
+ c = stream.read(len(null))
+ if c == null: break
+ string += c
+ return string.decode(self.encoding)
+
+ def sizeof(self):
+ return None
+
+
+class PString:
+
+ def __init__(self,encoding):
+ self.encoding = encoding
+
+ def pack(self,stream,string):
+ string = string.encode(self.encoding)
+ stream.write(bytes(chr(len(string))))
+ stream.write(string)
+
+ def unpack(self,stream):
+ length = ord(stream.read(1))
+ return stream.read(length).decode(self.encoding)
+
+ def sizeof(self):
+ return None
+
+
+class TerminatedList:
+
+ @classmethod
+ def terminator_predicate(cls,element):
+ return element == cls.terminator_element
+
+ @classmethod
+ def pack(cls,stream,elements):
+ for element in elements:
+ cls.element_type.pack(stream,element)
+ cls.element_type.pack(stream,cls.terminator_value)
+
+ @classmethod
+ def unpack(cls,stream):
+ elements = []
+ while True:
+ element = cls.element_type.unpack(stream)
+ if cls.terminator_predicate(element): break
+ elements.append(element)
+ return elements
+
+ @staticmethod
+ def sizeof():
+ return None
+
+
+class Field:
+
+ def __init__(self,name,field_type):
+ self.name = name
+ self.field_type = field_type
+
+ def pack(self,stream,struct):
+ self.field_type.pack(stream,getattr(struct,self.name))
+
+ def unpack(self,stream,struct):
+ setattr(struct,self.name,self.field_type.unpack(stream))
+
+ def sizeof(self):
+ return self.field_type.sizeof()
+
+ def equal(self,struct,other):
+ return getattr(struct,self.name) == getattr(other,self.name)
+
+
+class Padding:
+
+ def __init__(self,length,padding=b'\xFF'):
+ self.length = length
+ self.padding = padding
+
+ def pack(self,stream,struct):
+ stream.write(self.padding*self.length)
+
+ def unpack(self,stream,struct):
+ stream.read(self.length)
+
+ def sizeof(self):
+ return self.length
+
+ def equal(self,struct,other):
+ return True
+
+
+class StructClassDictionary(dict):
+
+ def __init__(self):
+ super().__init__()
+ self.struct_fields = []
+
+ def __setitem__(self,key,value):
+ if not key[:2] == key[-2:] == '__' and not hasattr(value,'__get__'):
+ self.struct_fields.append(Field(key,value))
+ elif key == '__padding__':
+ self.struct_fields.append(value)
+ else:
+ super().__setitem__(key,value)
+
+
+class StructMetaClass(type):
+
+ @classmethod
+ def __prepare__(metacls,cls,bases,replace_fields=False):
+ return StructClassDictionary()
+
+ def __new__(metacls,cls,bases,classdict,replace_fields=False):
+ struct_class = super().__new__(metacls,cls,bases,classdict)
+
+ if not classdict.struct_fields:
+ return struct_class
+
+ if not hasattr(struct_class,'struct_fields'):
+ struct_fields = classdict.struct_fields
+ elif replace_fields:
+ struct_fields = copy.copy(struct_class.struct_fields)
+ metacls.replace_fields(struct_fields,classdict.struct_fields)
+ else:
+ raise TypeError('Cannot add fields to struct')
+
+ def __eq__(self,other):
+ return all(field.equal(self,other) for field in struct_fields)
+
+ struct_class.struct_fields = struct_fields
+ struct_class.struct_size = metacls.calculate_struct_size(struct_fields)
+ struct_class.__eq__ = __eq__
+ return struct_class
+
+ def __init__(self,cls,bases,classdict,replace_fields=False):
+ super().__init__(cls,bases,classdict)
+
+ @staticmethod
+ def calculate_struct_size(struct_fields):
+ if any(field.sizeof() is None for field in struct_fields):
+ return None
+ return sum(field.sizeof() for field in struct_fields)
+
+ @staticmethod
+ def replace_fields(fields,replacement_fields):
+ for replacement_field in replacement_fields:
+ for i,field in enumerate(fields):
+ if hasattr(field,'name') and field.name == replacement_field.name:
+ fields[i] = replacement_field
+
+
+class Struct(metaclass=StructMetaClass):
+
+ __slots__ = tuple()
+
+ @classmethod
+ def pack(cls,stream,struct):
+ for field in cls.struct_fields:
+ field.pack(stream,struct)
+
+ @classmethod
+ def unpack(cls,stream):
+ struct = cls.__new__(cls)
+ for field in cls.struct_fields:
+ field.unpack(stream,struct)
+ return struct
+
+ @classmethod
+ def sizeof(cls):
+ return cls.struct_size
+
diff --git a/explorer_widget.py b/explorer_widget.py
new file mode 100644
index 0000000..b67c128
--- /dev/null
+++ b/explorer_widget.py
@@ -0,0 +1,92 @@
+from PyQt4 import QtCore,QtGui
+import qt
+import gx
+
+
+class TextureWrapper(qt.Wrapper):
+ name = qt.Wrapper.Property(str)
+ wrap_s = qt.Wrapper.Property(gx.WrapMode)
+ wrap_t = qt.Wrapper.Property(gx.WrapMode)
+ minification_filter = qt.Wrapper.Property(gx.FilterMode)
+ magnification_filter = qt.Wrapper.Property(gx.FilterMode)
+ minimum_lod = qt.Wrapper.Property(float)
+ maximum_lod = qt.Wrapper.Property(float)
+ lod_bias = qt.Wrapper.Property(float)
+ unknown0 = qt.Wrapper.Property(int)
+ unknown1 = qt.Wrapper.Property(int)
+ unknown2 = qt.Wrapper.Property(int)
+
+ def __init__(self,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+
+ self.wrap_s_changed.connect(self.on_wrap_s_changed)
+ self.wrap_t_changed.connect(self.on_wrap_t_changed)
+ self.minification_filter_changed.connect(self.on_minification_filter_changed)
+ self.magnification_filter_changed.connect(self.on_magnification_filter_changed)
+ self.minimum_lod_changed.connect(self.on_minimum_lod_changed)
+ self.maximum_lod_changed.connect(self.on_maximum_lod_changed)
+ self.lod_bias_changed.connect(self.on_lod_bias_changed)
+
+ @QtCore.pyqtSlot(gx.WrapMode)
+ def on_wrap_s_changed(self,value):
+ self.wrapped_object.gl_wrap_s_need_update = True
+
+ @QtCore.pyqtSlot(gx.WrapMode)
+ def on_wrap_t_changed(self,value):
+ self.wrapped_object.gl_wrap_t_need_update = True
+
+ @QtCore.pyqtSlot(gx.FilterMode)
+ def on_minification_filter_changed(self,value):
+ self.wrapped_object.gl_minification_filter_need_update = True
+
+ @QtCore.pyqtSlot(gx.FilterMode)
+ def on_magnification_filter_changed(self,value):
+ self.wrapped_object.gl_magnification_filter_need_update = True
+
+ @QtCore.pyqtSlot(float)
+ def on_minimum_lod_changed(self,value):
+ self.wrapped_object.gl_minimum_lod_need_update = True
+
+ @QtCore.pyqtSlot(float)
+ def on_maximum_lod_changed(self,value):
+ self.wrapped_object.gl_maximum_lod_need_update = True
+
+ @QtCore.pyqtSlot(float)
+ def on_lod_bias_changed(self,value):
+ self.wrapped_object.gl_lod_bias_need_update = True
+
+
+class TextureItem(QtGui.QTreeWidgetItem):
+
+ def __init__(self,texture):
+ super().__init__([texture.name])
+ self.texture = texture
+ self.texture.name_changed.connect(self.on_texture_name_changed)
+
+ @QtCore.pyqtSlot(str)
+ def on_texture_name_changed(self,name):
+ self.setText(0,name)
+
+
+class ExplorerWidget(QtGui.QTreeWidget):
+
+ currentTextureChanged = QtCore.pyqtSignal(object)
+
+ def __init__(self,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+ self.header().close()
+ self.currentItemChanged.connect(self.on_currentItemChanged)
+
+ self.texture_list = QtGui.QTreeWidgetItem(['Textures'])
+ self.addTopLevelItem(self.texture_list)
+
+ def setModel(self,model):
+ self.texture_list.takeChildren()
+ for texture in model.textures:
+ self.texture_list.addChild(TextureItem(TextureWrapper(texture)))
+
+ @QtCore.pyqtSlot(QtGui.QTreeWidgetItem,QtGui.QTreeWidgetItem)
+ def on_currentItemChanged(self,current,previous):
+ if isinstance(current,TextureItem):
+ self.currentTextureChanged.emit(current.texture)
+
diff --git a/forms.py b/forms.py
new file mode 100644
index 0000000..5ff07d5
--- /dev/null
+++ b/forms.py
@@ -0,0 +1,77 @@
+from PyQt4 import QtCore,QtGui,uic
+import gx
+
+
+class ViewSettingsForm(QtGui.QWidget):
+
+ def __init__(self,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+
+ self.ui = uic.loadUi('ui/ViewSettingsForm.ui',self)
+
+ def setViewer(self,viewer):
+ self.z_near.bindProperty(viewer,'z_near',viewer.z_near_changed)
+ self.z_far.bindProperty(viewer,'z_far',viewer.z_far_changed)
+ self.fov.bindProperty(viewer,'fov',viewer.fov_changed)
+ self.movement_speed.bindProperty(viewer,'movement_speed',viewer.movement_speed_changed)
+ self.rotation_speed.bindProperty(viewer,'rotation_speed',viewer.rotation_speed_changed)
+
+
+class TextureForm(QtGui.QWidget):
+
+ def __init__(self,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+
+ self.ui = uic.loadUi('ui/TextureForm.ui',self)
+
+ self.wrap_s.setItems([gx.CLAMP,gx.REPEAT,gx.MIRROR])
+ self.wrap_t.setItems([gx.CLAMP,gx.REPEAT,gx.MIRROR])
+ self.minification_filter.setItems([gx.NEAR,gx.LINEAR,gx.NEAR_MIP_NEAR,gx.LIN_MIP_NEAR,gx.NEAR_MIP_LIN,gx.LIN_MIP_LIN])
+ self.magnification_filter.setItems([gx.NEAR,gx.LINEAR])
+
+ @QtCore.pyqtSlot(object)
+ def setTexture(self,texture):
+ self.name.bindProperty(texture,'name',texture.name_changed)
+
+ self.image_format.setText(texture.image_format.name)
+ self.image_size.setText('{} x {}'.format(texture.width,texture.height))
+ self.image_levels.setText(str(len(texture.images)))
+
+ if texture.palette is not None:
+ self.palette_format.setText(texture.palette.palette_format.name)
+ self.palette_size.setText(str(len(texture.palette)))
+ else:
+ self.palette_format.setText('-')
+ self.palette_size.setText('-')
+
+ self.wrap_s.bindProperty(texture,'wrap_s',texture.wrap_s_changed)
+ self.wrap_t.bindProperty(texture,'wrap_t',texture.wrap_t_changed)
+
+ self.minification_filter.bindProperty(texture,'minification_filter',texture.minification_filter_changed)
+ self.magnification_filter.bindProperty(texture,'magnification_filter',texture.magnification_filter_changed)
+
+ self.minimum_lod.bindProperty(texture,'minimum_lod',texture.minimum_lod_changed)
+ self.maximum_lod.bindProperty(texture,'maximum_lod',texture.maximum_lod_changed)
+ self.lod_bias.bindProperty(texture,'lod_bias',texture.lod_bias_changed)
+
+ self.unknown0.bindProperty(texture,'unknown0',texture.unknown0_changed)
+ self.unknown1.bindProperty(texture,'unknown1',texture.unknown1_changed)
+ self.unknown2.bindProperty(texture,'unknown2',texture.unknown2_changed)
+
+ def setUndoStack(self,undo_stack):
+ self.name.setUndoStack(undo_stack)
+
+ self.wrap_s.setUndoStack(undo_stack)
+ self.wrap_t.setUndoStack(undo_stack)
+
+ self.minification_filter.setUndoStack(undo_stack)
+ self.magnification_filter.setUndoStack(undo_stack)
+
+ self.minimum_lod.setUndoStack(undo_stack)
+ self.maximum_lod.setUndoStack(undo_stack)
+ self.lod_bias.setUndoStack(undo_stack)
+
+ self.unknown0.setUndoStack(undo_stack)
+ self.unknown1.setUndoStack(undo_stack)
+ self.unknown2.setUndoStack(undo_stack)
+
diff --git a/gl.py b/gl.py
new file mode 100644
index 0000000..548da69
--- /dev/null
+++ b/gl.py
@@ -0,0 +1,256 @@
+import numpy
+from OpenGL.GL import *
+
+
+class Resource(GLuint):
+
+ def __hash__(self):
+ return object.__hash__(self)
+
+ def __int__(self):
+ return self.value
+
+ def __index__(self):
+ return self.value
+
+
+class Buffer(Resource):
+
+ def __init__(self):
+ glGenBuffers(1,self)
+
+ def __del__(self):
+ glDeleteBuffers(1,self)
+
+
+class VertexArray(Resource):
+
+ def __init__(self):
+ glGenVertexArrays(1,self)
+
+ def __del__(self):
+ glDeleteVertexArrays(1,self)
+
+
+class Shader(Resource):
+
+ def __init__(self,shader_type,source):
+ super().__init__(glCreateShader(shader_type))
+
+ glShaderSource(self,source)
+ glCompileShader(self)
+
+ if not glGetShaderiv(self,GL_COMPILE_STATUS):
+ raise RuntimeError('Compile failure: {}{}'.format(glGetShaderInfoLog(self).decode(),source))
+
+ def __del__(self):
+ glDeleteShader(self)
+
+
+class Program(Resource):
+
+ def __init__(self,*shaders):
+ super().__init__(glCreateProgram())
+
+ for shader in shaders:
+ glAttachShader(self,shader)
+
+ glLinkProgram(self)
+
+ if not glGetProgramiv(self,GL_LINK_STATUS):
+ raise RuntimeError('Link failure: {}'.format(glGetProgramInfoLog(self).decode()))
+
+ for shader in shaders:
+ glDetachShader(self,shader)
+
+ def __del__(self):
+ glDeleteProgram(self)
+
+
+class Texture(Resource):
+
+ def __init__(self):
+ super().__init__(glGenTextures(1))
+
+ def __del__(self):
+ glDeleteTextures(self.value)
+
+
+class Sampler(Resource):
+
+ def __init__(self):
+ glGenSamplers(1,self)
+
+ def __del__(self):
+ glDeleteSamplers(1,self)
+
+
+class Renderbuffer(Resource):
+
+ def __init__(self):
+ glGenRenderbuffers(1,self)
+
+ def __del__(self):
+ glDeleteRenderbuffers(1,self)
+
+
+class Framebuffer(Resource):
+
+ def __init__(self):
+ glGenFramebuffers(1,self)
+
+ def __del__(self):
+ glDeleteFramebuffers(1,self)
+
+
+class ChangeRegisteringArray(numpy.ndarray):
+ #XXX Should only be changed using __setitem__
+
+ def __array_finalize__(self,obj):
+ if obj is None:
+ self.changed = True
+
+ def __setitem__(self,*args):
+ if self.base is None:
+ self.changed = True
+ else:
+ self.base.changed = True
+ super().__setitem__(*args)
+
+
+class ManagedBuffer:
+
+ def __init__(self,target,usage,*args,**kwargs):
+ super().__init__()
+ self.target = target
+ self.usage = usage
+ self.data = ChangeRegisteringArray(*args,**kwargs)
+ self.buffer = Buffer()
+ glBindBuffer(target,self.buffer)
+ glBufferData(target,self.data.nbytes,None,usage)
+
+ def __getitem__(self,key):
+ return self.data[key]
+
+ def __setitem__(self,key,value):
+ self.data[key] = value
+
+ def __iter__(self):
+ return iter(self.data)
+
+ def sync_data(self):
+ if self.data.changed:
+ glBindBuffer(self.target,self.buffer)
+ glBufferSubData(self.target,0,self.data.nbytes,self.data.view(numpy.ndarray))
+ self.data.changed = False
+
+ def bind(self,binding_point=None):
+ if binding_point is None:
+ glBindBuffer(self.target,self.buffer)
+ else:
+ glBindBufferBase(self.target,binding_point,self.buffer)
+
+ if self.data.changed:
+ glBufferSubData(self.target,0,self.data.nbytes,self.data.view(numpy.ndarray))
+ self.data.changed = False
+
+
+class TextureBuffer(ManagedBuffer):
+
+ def __init__(self,usage,element_type,*args,**kwargs):
+ super().__init__(GL_TEXTURE_BUFFER,usage,*args,**kwargs)
+ self.element_type = element_type
+ self.texture = Texture()
+
+ def bind_texture(self,texture_unit):
+ self.sync_data()
+ glActiveTexture(GL_TEXTURE0 + texture_unit)
+ glBindTexture(GL_TEXTURE_BUFFER,self.texture)
+ glTexBuffer(GL_TEXTURE_BUFFER,self.element_type,self.buffer)
+
+
+class Type:
+
+ def __init__(self,glsl_type,numpy_type):
+ self.glsl_type = glsl_type
+ self.numpy_type = numpy_type
+
+ if not numpy_type.shape:
+ self.base_alignment = 4
+ elif len(numpy_type.shape) == 1:
+ self.base_alignment = 16 if numpy_type.shape[0] != 2 else 8
+ else:
+ self.base_alignment = 16
+ if numpy_type.shape[-1] % 4 != 0:
+ raise Exception('unaligned array elements not implemented')
+
+
+vec2 = Type('vec2',numpy.dtype((numpy.float32,2)))
+vec3 = Type('vec3',numpy.dtype((numpy.float32,3)))
+vec4 = Type('vec4',numpy.dtype((numpy.float32,4)))
+mat3x2 = Type('mat3x2',numpy.dtype((numpy.float32,(2,4)))) #TODO
+mat4x2 = Type('mat4x2',numpy.dtype((numpy.float32,(2,4))))
+mat4x3 = Type('mat4x3',numpy.dtype((numpy.float32,(3,4))))
+mat4 = Type('mat4',numpy.dtype((numpy.float32,(4,4))))
+
+
+class UniformBlockClassDictionary(dict):
+
+ def __init__(self):
+ super().__init__()
+ self.glsl_fields = []
+ self.numpy_fields = []
+ self.offset = 0
+
+ def add_field(self,name,field_type):
+ if self.offset % field_type.base_alignment != 0:
+ raise Exception('unaligned fields not implemented')
+ self.glsl_fields.append('{} {};'.format(field_type.glsl_type,name))
+ self.numpy_fields.append((name,field_type.numpy_type))
+ self.offset += field_type.numpy_type.itemsize
+
+ def __setitem__(self,key,value):
+ if not key[:2] == key[-2:] == '__' and not hasattr(value,'__get__'):
+ self.add_field(key,value)
+ else:
+ super().__setitem__(key,value)
+
+
+class UniformBlockMetaClass(type):
+
+ @classmethod
+ def __prepare__(metacls,cls,bases):
+ return UniformBlockClassDictionary()
+
+ def __new__(metacls,cls,bases,classdict):
+ uniform_block_class = type.__new__(metacls,cls,bases,classdict)
+ uniform_block_class.glsl_type = (
+ 'layout(std140,row_major) uniform {}\n'.format(cls) +
+ '{\n' +
+ ''.join(' {}\n'.format(field) for field in classdict.glsl_fields) +
+ '};')
+ uniform_block_class.numpy_type = numpy.dtype(classdict.numpy_fields)
+ return uniform_block_class
+
+
+class UniformBlock(ManagedBuffer,metaclass=UniformBlockMetaClass):
+
+ def __init__(self,usage):
+ super().__init__(GL_UNIFORM_BUFFER,usage,1,self.numpy_type)
+
+ def __getitem__(self,key):
+ return super().__getitem__(key)[0]
+
+ def __setitem__(self,key,value):
+ super().__getitem__(key)[0] = value
+
+
+def uniform_block(class_name,fields):
+ bases = (UniformBlock,)
+ classdict = UniformBlockMetaClass.__prepare__(class_name,bases)
+
+ for name,field_type in fields:
+ classdict[name] = field_type
+
+ return UniformBlockMetaClass(class_name,bases,classdict)
+
diff --git a/gx/__init__.py b/gx/__init__.py
new file mode 100644
index 0000000..0e38225
--- /dev/null
+++ b/gx/__init__.py
@@ -0,0 +1,2 @@
+from gx.constants import *
+
diff --git a/gx/constants.py b/gx/constants.py
new file mode 100644
index 0000000..1bb044d
--- /dev/null
+++ b/gx/constants.py
@@ -0,0 +1,1209 @@
+import enum
+import numpy
+from OpenGL.GL import *
+
+
+class Value(int):
+
+ def __new__(cls,value,*args,**kwargs):
+ return super().__new__(cls,value)
+
+ def __init__(self,value,**attributes):
+ self.attributes = attributes
+
+
+class Enum(int,enum.Enum):
+
+ def __init__(self,value):
+ if hasattr(value,'attributes'):
+ for attribute in value.attributes.items():
+ setattr(self,*attribute)
+
+
+class ExtendEnumMeta(enum.EnumMeta):
+
+ def __new__(metacls,cls,bases,classdict):
+ (base_enum,) = bases
+ enum_class = super().__new__(metacls,cls,base_enum.__bases__,classdict)
+ enum_class.base_enum = base_enum
+ return enum_class
+
+ #TODO: __dir__
+
+ def __call__(self,*args,**kwargs):
+ try:
+ return self.base_enum(*args,**kwargs)
+ except ValueError:
+ return super().__call__(*args,**kwargs)
+
+ def __getattr__(self,name):
+ try:
+ return getattr(self.base_enum,name)
+ except AttributeError:
+ return super().__getattr__(name)
+
+ def __getitem__(self,name):
+ try:
+ return self.base_enum[name]
+ except KeyError:
+ return super().__getitem__(name)
+
+ def __contains__(self,member):
+ return member in self.base_enum or super().__contains__(member)
+
+ def __len__(self):
+ return len(self.base_enum) + super().__len__()
+
+ def __iter__(self):
+ yield from self.base_enum
+ yield from super().__iter__()
+
+ def __reversed__(self):
+ yield from super().__reversed__()
+ yield from reversed(self.base_enum)
+
+ #TODO: __members__
+
+
+class Attribute(Enum):
+ VA_PTNMTXIDX = 0
+ VA_TEX0MTXIDX = Value(1,index=0)
+ VA_TEX1MTXIDX = Value(2,index=1)
+ VA_TEX2MTXIDX = Value(3,index=2)
+ VA_TEX3MTXIDX = Value(4,index=3)
+ VA_TEX4MTXIDX = Value(5,index=4)
+ VA_TEX5MTXIDX = Value(6,index=5)
+ VA_TEX6MTXIDX = Value(7,index=6)
+ VA_TEX7MTXIDX = Value(8,index=7)
+ VA_POS = 9
+ VA_NRM = 10
+ VA_CLR0 = Value(11,index=0)
+ VA_CLR1 = Value(12,index=1)
+ VA_TEX0 = Value(13,index=0)
+ VA_TEX1 = Value(14,index=1)
+ VA_TEX2 = Value(15,index=2)
+ VA_TEX3 = Value(16,index=3)
+ VA_TEX4 = Value(17,index=4)
+ VA_TEX5 = Value(18,index=5)
+ VA_TEX6 = Value(19,index=6)
+ VA_TEX7 = Value(20,index=7)
+ POSMTXARRAY = 21
+ NRMMTXARRAY = 22
+ TEXMTXARRAY = 23
+ LIGHTARRAY = 24
+ VA_NBT = 25
+ VA_NULL = 0xFF
+
+
+VA_PTNMTXIDX = Attribute.VA_PTNMTXIDX
+VA_TEX0MTXIDX = Attribute.VA_TEX0MTXIDX
+VA_TEX1MTXIDX = Attribute.VA_TEX1MTXIDX
+VA_TEX2MTXIDX = Attribute.VA_TEX2MTXIDX
+VA_TEX3MTXIDX = Attribute.VA_TEX3MTXIDX
+VA_TEX4MTXIDX = Attribute.VA_TEX4MTXIDX
+VA_TEX5MTXIDX = Attribute.VA_TEX5MTXIDX
+VA_TEX6MTXIDX = Attribute.VA_TEX6MTXIDX
+VA_TEX7MTXIDX = Attribute.VA_TEX7MTXIDX
+VA_TEXMTXIDX = [VA_TEX0MTXIDX,VA_TEX1MTXIDX,VA_TEX2MTXIDX,VA_TEX3MTXIDX,VA_TEX4MTXIDX,VA_TEX5MTXIDX,VA_TEX6MTXIDX,VA_TEX7MTXIDX]
+VA_POS = Attribute.VA_POS
+VA_NRM = Attribute.VA_NRM
+VA_CLR0 = Attribute.VA_CLR0
+VA_CLR1 = Attribute.VA_CLR1
+VA_CLR = [VA_CLR0,VA_CLR1]
+VA_TEX0 = Attribute.VA_TEX0
+VA_TEX1 = Attribute.VA_TEX1
+VA_TEX2 = Attribute.VA_TEX2
+VA_TEX3 = Attribute.VA_TEX3
+VA_TEX4 = Attribute.VA_TEX4
+VA_TEX5 = Attribute.VA_TEX5
+VA_TEX6 = Attribute.VA_TEX6
+VA_TEX7 = Attribute.VA_TEX7
+VA_TEX = [VA_TEX0,VA_TEX1,VA_TEX2,VA_TEX3,VA_TEX4,VA_TEX5,VA_TEX6,VA_TEX7]
+POSMTXARRAY = Attribute.POSMTXARRAY
+NRMMTXARRAY = Attribute.NRMMTXARRAY
+TEXMTXARRAY = Attribute.TEXMTXARRAY
+LIGHTARRAY = Attribute.LIGHTARRAY
+VA_NBT = Attribute.VA_NBT
+VA_NULL = Attribute.VA_NULL
+
+
+class ComponentType(Enum):
+ U8 = Value(0,numpy_type=numpy.uint8)
+ S8 = Value(1,numpy_type=numpy.int8)
+ U16 = Value(2,numpy_type=numpy.uint16)
+ S16 = Value(3,numpy_type=numpy.int16)
+ F32 = Value(4,numpy_type=numpy.float32)
+
+
+U8 = ComponentType.U8
+S8 = ComponentType.S8
+U16 = ComponentType.U16
+S16 = ComponentType.S16
+F32 = ComponentType.F32
+
+
+class ColorComponentType(Enum):
+ RGB565 = 0
+ RGB8 = 1
+ RGBX8 = 2
+ RGBA4 = 3
+ RGBA6 = 4
+ RGBA8 = 5
+
+
+RGB565 = ColorComponentType.RGB565
+RGB8 = ColorComponentType.RGB8
+RGBX8 = ColorComponentType.RGBX8
+RGBA4 = ColorComponentType.RGBA4
+RGBA6 = ColorComponentType.RGBA6
+RGBA8 = ColorComponentType.RGBA8
+
+
+class PositionComponentCount(Enum):
+ POS_XY = Value(0,actual_value=2)
+ POS_XYZ = Value(1,actual_value=3)
+
+
+POS_XY = PositionComponentCount.POS_XY
+POS_XYZ = PositionComponentCount.POS_XYZ
+
+
+class NormalComponentCount(Enum):
+ NRM_XYZ = Value(0,actual_value=3)
+ NRM_NBT = 1
+ NRM_NBT3 = 2
+
+
+NRM_XYZ = NormalComponentCount.NRM_XYZ
+NRM_NBT = NormalComponentCount.NRM_NBT
+NRM_NBT3 = NormalComponentCount.NRM_NBT3
+
+
+class ColorComponentCount(Enum):
+ CLR_RGB = 0
+ CLR_RGBA = 1
+
+
+CLR_RGB = ColorComponentCount.CLR_RGB
+CLR_RGBA = ColorComponentCount.CLR_RGBA
+
+
+class TexCoordComponentCount(Enum):
+ TEX_S = Value(0,actual_value=1)
+ TEX_ST = Value(1,actual_value=2)
+
+
+TEX_S = TexCoordComponentCount.TEX_S
+TEX_ST = TexCoordComponentCount.TEX_ST
+
+
+class InputType(Enum):
+ NONE = 0
+ DIRECT = 1
+ INDEX8 = 2
+ INDEX16 = 3
+
+
+NONE = InputType.NONE
+DIRECT = InputType.DIRECT
+INDEX8 = InputType.INDEX8
+INDEX16 = InputType.INDEX16
+
+
+class PrimitiveType(Enum):
+ POINTS = 0xB8
+ LINES = 0xA8
+ LINESTRIP = 0xB0
+ TRIANGLES = 0x90
+ TRIANGLESTRIP = 0x98
+ TRIANGLEFAN = 0xA0
+ QUADS = 0x80
+
+
+POINTS = PrimitiveType.POINTS
+LINES = PrimitiveType.LINES
+LINESTRIP = PrimitiveType.LINESTRIP
+TRIANGLES = PrimitiveType.TRIANGLES
+TRIANGLESTRIP = PrimitiveType.TRIANGLESTRIP
+TRIANGLEFAN = PrimitiveType.TRIANGLEFAN
+QUADS = PrimitiveType.QUADS
+
+
+class Channel(Enum):
+ COLOR0 = Value(0,index=0)
+ COLOR1 = Value(1,index=1)
+ ALPHA0 = Value(2,index=0)
+ ALPHA1 = Value(3,index=1)
+ COLOR0A0 = Value(4,index=0)
+ COLOR1A1 = Value(5,index=1)
+ COLOR_ZERO = 6
+ ALPHA_BUMP = 7
+ ALPHA_BUMPN = 8
+ COLOR_NULL = 0xFF
+
+
+COLOR0 = Channel.COLOR0
+COLOR1 = Channel.COLOR1
+COLOR = [COLOR0,COLOR1]
+ALPHA0 = Channel.ALPHA0
+ALPHA1 = Channel.ALPHA1
+ALPHA = [ALPHA0,ALPHA1]
+COLOR0A0 = Channel.COLOR0A0
+COLOR1A1 = Channel.COLOR1A1
+COLOR_ZERO = Channel.COLOR_ZERO
+ALPHA_BUMP = Channel.ALPHA_BUMP
+ALPHA_BUMPN = Channel.ALPHA_BUMPN
+COLOR_NULL = Channel.COLOR_NULL
+
+
+class ChannelSource(Enum):
+ SRC_REG = 0
+ SRC_VTX = 1
+
+
+SRC_REG = ChannelSource.SRC_REG
+SRC_VTX = ChannelSource.SRC_VTX
+
+
+class Light(Enum):
+ LIGHT0 = Value(0x01,index=0)
+ LIGHT1 = Value(0x02,index=0)
+ LIGHT2 = Value(0x04,index=0)
+ LIGHT3 = Value(0x08,index=0)
+ LIGHT4 = Value(0x10,index=0)
+ LIGHT5 = Value(0x20,index=0)
+ LIGHT6 = Value(0x40,index=0)
+ LIGHT7 = Value(0x80,index=0)
+ LIGHT_NULL = 0x00
+
+
+LIGHT0 = Light.LIGHT0
+LIGHT1 = Light.LIGHT1
+LIGHT2 = Light.LIGHT2
+LIGHT3 = Light.LIGHT3
+LIGHT4 = Light.LIGHT4
+LIGHT5 = Light.LIGHT5
+LIGHT6 = Light.LIGHT6
+LIGHT7 = Light.LIGHT7
+LIGHT = [LIGHT0,LIGHT1,LIGHT2,LIGHT3,LIGHT4,LIGHT5,LIGHT6,LIGHT7]
+LIGHT_NULL = Light.LIGHT_NULL
+
+
+class DiffuseFunction(Enum):
+ DF_NONE = 0
+ DF_SIGNED = 1
+ DF_CLAMP = 2
+
+
+DF_NONE = DiffuseFunction.DF_NONE
+DF_SIGNED = DiffuseFunction.DF_SIGNED
+DF_CLAMP = DiffuseFunction.DF_CLAMP
+
+
+class AttenuationFunction(Enum):
+ AF_SPEC = 0
+ AF_SPOT = 1
+ AF_NONE = 2
+
+
+AF_SPEC = AttenuationFunction.AF_SPEC
+AF_SPOT = AttenuationFunction.AF_SPOT
+AF_NONE = AttenuationFunction.AF_NONE
+
+
+class TexCoord(Enum):
+ TEXCOORD0 = Value(0,index=0)
+ TEXCOORD1 = Value(1,index=1)
+ TEXCOORD2 = Value(2,index=2)
+ TEXCOORD3 = Value(3,index=3)
+ TEXCOORD4 = Value(4,index=4)
+ TEXCOORD5 = Value(5,index=5)
+ TEXCOORD6 = Value(6,index=6)
+ TEXCOORD7 = Value(7,index=7)
+ TEXCOORD_NULL = 0xFF
+
+
+TEXCOORD0 = TexCoord.TEXCOORD0
+TEXCOORD1 = TexCoord.TEXCOORD1
+TEXCOORD2 = TexCoord.TEXCOORD2
+TEXCOORD3 = TexCoord.TEXCOORD3
+TEXCOORD4 = TexCoord.TEXCOORD4
+TEXCOORD5 = TexCoord.TEXCOORD5
+TEXCOORD6 = TexCoord.TEXCOORD6
+TEXCOORD7 = TexCoord.TEXCOORD7
+TEXCOORD = [TEXCOORD0,TEXCOORD1,TEXCOORD2,TEXCOORD3,TEXCOORD4,TEXCOORD5,TEXCOORD6,TEXCOORD7]
+TEXCOORD_NULL = TexCoord.TEXCOORD_NULL
+
+
+class TexCoordFunction(Enum):
+ TG_MTX3x4 = 0
+ TG_MTX2x4 = 1
+ TG_BUMP0 = Value(2,index=0)
+ TG_BUMP1 = Value(3,index=1)
+ TG_BUMP2 = Value(4,index=2)
+ TG_BUMP3 = Value(5,index=3)
+ TG_BUMP4 = Value(6,index=4)
+ TG_BUMP5 = Value(7,index=5)
+ TG_BUMP6 = Value(8,index=6)
+ TG_BUMP7 = Value(9,index=7)
+ TG_SRTG = 10
+
+
+TG_MTX3x4 = TexCoordFunction.TG_MTX3x4
+TG_MTX2x4 = TexCoordFunction.TG_MTX2x4
+TG_BUMP0 = TexCoordFunction.TG_BUMP0
+TG_BUMP1 = TexCoordFunction.TG_BUMP1
+TG_BUMP2 = TexCoordFunction.TG_BUMP2
+TG_BUMP3 = TexCoordFunction.TG_BUMP3
+TG_BUMP4 = TexCoordFunction.TG_BUMP4
+TG_BUMP5 = TexCoordFunction.TG_BUMP5
+TG_BUMP6 = TexCoordFunction.TG_BUMP6
+TG_BUMP7 = TexCoordFunction.TG_BUMP7
+TG_BUMP = [TG_BUMP0,TG_BUMP1,TG_BUMP2,TG_BUMP3,TG_BUMP4,TG_BUMP5,TG_BUMP6,TG_BUMP7]
+TG_SRTG = TexCoordFunction.TG_SRTG
+
+
+class TexCoordSource(Enum):
+ TG_POS = 0
+ TG_NRM = 1
+ TG_BINRM = 2
+ TG_TANGENT = 3
+ TG_TEX0 = Value(4,index=0)
+ TG_TEX1 = Value(5,index=1)
+ TG_TEX2 = Value(6,index=2)
+ TG_TEX3 = Value(7,index=3)
+ TG_TEX4 = Value(8,index=4)
+ TG_TEX5 = Value(9,index=5)
+ TG_TEX6 = Value(10,index=6)
+ TG_TEX7 = Value(11,index=7)
+ TG_TEXCOORD0 = Value(12,index=0)
+ TG_TEXCOORD1 = Value(13,index=1)
+ TG_TEXCOORD2 = Value(14,index=2)
+ TG_TEXCOORD3 = Value(15,index=3)
+ TG_TEXCOORD4 = Value(16,index=4)
+ TG_TEXCOORD5 = Value(17,index=5)
+ TG_TEXCOORD6 = Value(18,index=6)
+ TG_COLOR0 = Value(19,index=0)
+ TG_COLOR1 = Value(20,index=1)
+
+
+TG_POS = TexCoordSource.TG_POS
+TG_NRM = TexCoordSource.TG_NRM
+TG_BINRM = TexCoordSource.TG_BINRM
+TG_TANGENT = TexCoordSource.TG_TANGENT
+TG_TEX0 = TexCoordSource.TG_TEX0
+TG_TEX1 = TexCoordSource.TG_TEX1
+TG_TEX2 = TexCoordSource.TG_TEX2
+TG_TEX3 = TexCoordSource.TG_TEX3
+TG_TEX4 = TexCoordSource.TG_TEX4
+TG_TEX5 = TexCoordSource.TG_TEX5
+TG_TEX6 = TexCoordSource.TG_TEX6
+TG_TEX7 = TexCoordSource.TG_TEX7
+TG_TEX = [TG_TEX0,TG_TEX1,TG_TEX2,TG_TEX3,TG_TEX4,TG_TEX5,TG_TEX6,TG_TEX7]
+TG_TEXCOORD0 = TexCoordSource.TG_TEXCOORD0
+TG_TEXCOORD1 = TexCoordSource.TG_TEXCOORD1
+TG_TEXCOORD2 = TexCoordSource.TG_TEXCOORD2
+TG_TEXCOORD3 = TexCoordSource.TG_TEXCOORD3
+TG_TEXCOORD4 = TexCoordSource.TG_TEXCOORD4
+TG_TEXCOORD5 = TexCoordSource.TG_TEXCOORD5
+TG_TEXCOORD6 = TexCoordSource.TG_TEXCOORD6
+TG_TEXCOORD = [TG_TEXCOORD0,TG_TEXCOORD1,TG_TEXCOORD2,TG_TEXCOORD3,TG_TEXCOORD4,TG_TEXCOORD5,TG_TEXCOORD6]
+TG_COLOR0 = TexCoordSource.TG_COLOR0
+TG_COLOR1 = TexCoordSource.TG_COLOR1
+TG_COLOR = [TG_COLOR0,TG_COLOR1]
+
+
+class TextureMatrix(Enum):
+ TEXMTX0 = Value(30,index=0)
+ TEXMTX1 = Value(33,index=1)
+ TEXMTX2 = Value(36,index=2)
+ TEXMTX3 = Value(39,index=3)
+ TEXMTX4 = Value(42,index=4)
+ TEXMTX5 = Value(45,index=5)
+ TEXMTX6 = Value(48,index=6)
+ TEXMTX7 = Value(51,index=7)
+ TEXMTX8 = Value(54,index=8)
+ TEXMTX9 = Value(57,index=9)
+ IDENTITY = 60
+
+
+TEXMTX0 = TextureMatrix.TEXMTX0
+TEXMTX1 = TextureMatrix.TEXMTX1
+TEXMTX2 = TextureMatrix.TEXMTX2
+TEXMTX3 = TextureMatrix.TEXMTX3
+TEXMTX4 = TextureMatrix.TEXMTX4
+TEXMTX5 = TextureMatrix.TEXMTX5
+TEXMTX6 = TextureMatrix.TEXMTX6
+TEXMTX7 = TextureMatrix.TEXMTX7
+TEXMTX8 = TextureMatrix.TEXMTX8
+TEXMTX9 = TextureMatrix.TEXMTX9
+TEXMTX = [TEXMTX0,TEXMTX1,TEXMTX2,TEXMTX3,TEXMTX4,TEXMTX5,TEXMTX6,TEXMTX7,TEXMTX8,TEXMTX9]
+IDENTITY = TextureMatrix.IDENTITY
+
+
+class PostTransformMatrix(Enum):
+ PTTMTX0 = Value(64,index=0)
+ PTTMTX1 = Value(67,index=1)
+ PTTMTX2 = Value(70,index=2)
+ PTTMTX3 = Value(73,index=3)
+ PTTMTX4 = Value(76,index=4)
+ PTTMTX5 = Value(79,index=5)
+ PTTMTX6 = Value(82,index=6)
+ PTTMTX7 = Value(85,index=7)
+ PTTMTX8 = Value(88,index=8)
+ PTTMTX9 = Value(91,index=9)
+ PTTMTX10 = Value(94,index=10)
+ PTTMTX11 = Value(97,index=11)
+ PTTMTX12 = Value(100,index=12)
+ PTTMTX13 = Value(103,index=13)
+ PTTMTX14 = Value(106,index=14)
+ PTTMTX15 = Value(109,index=15)
+ PTTMTX16 = Value(112,index=16)
+ PTTMTX17 = Value(115,index=17)
+ PTTMTX18 = Value(118,index=18)
+ PTTMTX19 = Value(121,index=19)
+ PTTIDENTITY = 125
+
+
+PTTMTX0 = PostTransformMatrix.PTTMTX0
+PTTMTX1 = PostTransformMatrix.PTTMTX1
+PTTMTX2 = PostTransformMatrix.PTTMTX2
+PTTMTX3 = PostTransformMatrix.PTTMTX3
+PTTMTX4 = PostTransformMatrix.PTTMTX4
+PTTMTX5 = PostTransformMatrix.PTTMTX5
+PTTMTX6 = PostTransformMatrix.PTTMTX6
+PTTMTX7 = PostTransformMatrix.PTTMTX7
+PTTMTX8 = PostTransformMatrix.PTTMTX8
+PTTMTX9 = PostTransformMatrix.PTTMTX9
+PTTMTX10 = PostTransformMatrix.PTTMTX10
+PTTMTX11 = PostTransformMatrix.PTTMTX11
+PTTMTX12 = PostTransformMatrix.PTTMTX12
+PTTMTX13 = PostTransformMatrix.PTTMTX13
+PTTMTX14 = PostTransformMatrix.PTTMTX14
+PTTMTX15 = PostTransformMatrix.PTTMTX15
+PTTMTX16 = PostTransformMatrix.PTTMTX16
+PTTMTX17 = PostTransformMatrix.PTTMTX17
+PTTMTX18 = PostTransformMatrix.PTTMTX18
+PTTMTX19 = PostTransformMatrix.PTTMTX19
+PTTMTX = [PTTMTX0,PTTMTX1,PTTMTX2,PTTMTX3,PTTMTX4,PTTMTX5,PTTMTX6,PTTMTX7,PTTMTX8,PTTMTX9,PTTMTX10,PTTMTX11,PTTMTX12,PTTMTX13,PTTMTX14,PTTMTX15,PTTMTX16,PTTMTX17,PTTMTX18,PTTMTX19]
+PTTIDENTITY = PostTransformMatrix.PTTIDENTITY
+
+
+class Texture(Enum):
+ TEXMAP0 = Value(0,index=0)
+ TEXMAP1 = Value(1,index=1)
+ TEXMAP2 = Value(2,index=2)
+ TEXMAP3 = Value(3,index=3)
+ TEXMAP4 = Value(4,index=4)
+ TEXMAP5 = Value(5,index=5)
+ TEXMAP6 = Value(6,index=6)
+ TEXMAP7 = Value(7,index=7)
+ TEXMAP_NULL = 0xFF
+ TEXMAP_DISABLE = 0x100
+
+
+TEXMAP0 = Texture.TEXMAP0
+TEXMAP1 = Texture.TEXMAP1
+TEXMAP2 = Texture.TEXMAP2
+TEXMAP3 = Texture.TEXMAP3
+TEXMAP4 = Texture.TEXMAP4
+TEXMAP5 = Texture.TEXMAP5
+TEXMAP6 = Texture.TEXMAP6
+TEXMAP7 = Texture.TEXMAP7
+TEXMAP = [TEXMAP0,TEXMAP1,TEXMAP2,TEXMAP3,TEXMAP4,TEXMAP5,TEXMAP6,TEXMAP7]
+TEXMAP_NULL = Texture.TEXMAP_NULL
+TEXMAP_DISABLE = Texture.TEXMAP_DISABLE
+
+
+class TextureFormat(Enum):
+ TF_I4 = 0
+ TF_I8 = 1
+ TF_IA4 = 2
+ TF_IA8 = 3
+ TF_RGB565 = 4
+ TF_RGB5A3 = 5
+ TF_RGBA8 = 6
+ TF_CI4 = 8
+ TF_CI8 = 9
+ TF_CI14 = 10
+ TF_CMPR = 14
+
+
+TF_I4 = TextureFormat.TF_I4
+TF_I8 = TextureFormat.TF_I8
+TF_IA4 = TextureFormat.TF_IA4
+TF_IA8 = TextureFormat.TF_IA8
+TF_RGB565 = TextureFormat.TF_RGB565
+TF_RGB5A3 = TextureFormat.TF_RGB5A3
+TF_RGBA8 = TextureFormat.TF_RGBA8
+TF_CI4 = TextureFormat.TF_CI4
+TF_CI8 = TextureFormat.TF_CI8
+TF_CI14 = TextureFormat.TF_CI14
+TF_CMPR = TextureFormat.TF_CMPR
+
+
+class PaletteFormat(Enum):
+ TL_IA8 = 0
+ TL_RGB565 = 1
+ TL_RGB5A3 = 2
+
+
+TL_IA8 = PaletteFormat.TL_IA8
+TL_RGB565 = PaletteFormat.TL_RGB565
+TL_RGB5A3 = PaletteFormat.TL_RGB5A3
+
+
+class WrapMode(Enum):
+ CLAMP = Value(0,gl_value=GL_CLAMP_TO_EDGE)
+ REPEAT = Value(1,gl_value=GL_REPEAT)
+ MIRROR = Value(2,gl_value=GL_MIRRORED_REPEAT)
+
+
+CLAMP = WrapMode.CLAMP
+REPEAT = WrapMode.REPEAT
+MIRROR = WrapMode.MIRROR
+
+
+class FilterMode(Enum):
+ NEAR = Value(0,gl_value=GL_NEAREST)
+ LINEAR = Value(1,gl_value=GL_LINEAR)
+ NEAR_MIP_NEAR = Value(2,gl_value=GL_NEAREST_MIPMAP_NEAREST)
+ LIN_MIP_NEAR = Value(3,gl_value=GL_LINEAR_MIPMAP_NEAREST)
+ NEAR_MIP_LIN = Value(4,gl_value=GL_NEAREST_MIPMAP_LINEAR)
+ LIN_MIP_LIN = Value(5,gl_value=GL_LINEAR_MIPMAP_LINEAR)
+
+
+NEAR = FilterMode.NEAR
+LINEAR = FilterMode.LINEAR
+NEAR_MIP_NEAR = FilterMode.NEAR_MIP_NEAR
+LIN_MIP_NEAR = FilterMode.LIN_MIP_NEAR
+NEAR_MIP_LIN = FilterMode.NEAR_MIP_LIN
+LIN_MIP_LIN = FilterMode.LIN_MIP_LIN
+
+
+class TevStage(Enum):
+ TEVSTAGE0 = Value(0,index=0)
+ TEVSTAGE1 = Value(1,index=1)
+ TEVSTAGE2 = Value(2,index=2)
+ TEVSTAGE3 = Value(3,index=3)
+ TEVSTAGE4 = Value(4,index=4)
+ TEVSTAGE5 = Value(5,index=5)
+ TEVSTAGE6 = Value(6,index=6)
+ TEVSTAGE7 = Value(7,index=7)
+ TEVSTAGE8 = Value(8,index=8)
+ TEVSTAGE9 = Value(9,index=9)
+ TEVSTAGE10 = Value(10,index=10)
+ TEVSTAGE11 = Value(11,index=11)
+ TEVSTAGE12 = Value(12,index=12)
+ TEVSTAGE13 = Value(13,index=13)
+ TEVSTAGE14 = Value(14,index=14)
+ TEVSTAGE15 = Value(15,index=15)
+
+
+TEVSTAGE0 = TevStage.TEVSTAGE0
+TEVSTAGE1 = TevStage.TEVSTAGE1
+TEVSTAGE2 = TevStage.TEVSTAGE2
+TEVSTAGE3 = TevStage.TEVSTAGE3
+TEVSTAGE4 = TevStage.TEVSTAGE4
+TEVSTAGE5 = TevStage.TEVSTAGE5
+TEVSTAGE6 = TevStage.TEVSTAGE6
+TEVSTAGE7 = TevStage.TEVSTAGE7
+TEVSTAGE8 = TevStage.TEVSTAGE8
+TEVSTAGE9 = TevStage.TEVSTAGE9
+TEVSTAGE10 = TevStage.TEVSTAGE10
+TEVSTAGE11 = TevStage.TEVSTAGE11
+TEVSTAGE12 = TevStage.TEVSTAGE12
+TEVSTAGE13 = TevStage.TEVSTAGE13
+TEVSTAGE14 = TevStage.TEVSTAGE14
+TEVSTAGE15 = TevStage.TEVSTAGE15
+TEVSTAGE = [TEVSTAGE0,TEVSTAGE1,TEVSTAGE2,TEVSTAGE3,TEVSTAGE4,TEVSTAGE5,TEVSTAGE6,TEVSTAGE7,TEVSTAGE8,TEVSTAGE9,TEVSTAGE10,TEVSTAGE11,TEVSTAGE12,TEVSTAGE13,TEVSTAGE14,TEVSTAGE15]
+
+
+class ColorInput(Enum):
+ CC_CPREV = 0
+ CC_APREV = 1
+ CC_C0 = 2
+ CC_A0 = 3
+ CC_C1 = 4
+ CC_A1 = 5
+ CC_C2 = 6
+ CC_A2 = 7
+ CC_TEXC = 8
+ CC_TEXA = 9
+ CC_RASC = 10
+ CC_RASA = 11
+ CC_ONE = 12
+ CC_HALF = 13
+ CC_KONST = 14
+ CC_ZERO = 15
+
+
+CC_CPREV = ColorInput.CC_CPREV
+CC_APREV = ColorInput.CC_APREV
+CC_C0 = ColorInput.CC_C0
+CC_A0 = ColorInput.CC_A0
+CC_C1 = ColorInput.CC_C1
+CC_A1 = ColorInput.CC_A1
+CC_C2 = ColorInput.CC_C2
+CC_A2 = ColorInput.CC_A2
+CC_TEXC = ColorInput.CC_TEXC
+CC_TEXA = ColorInput.CC_TEXA
+CC_RASC = ColorInput.CC_RASC
+CC_RASA = ColorInput.CC_RASA
+CC_ONE = ColorInput.CC_ONE
+CC_HALF = ColorInput.CC_HALF
+CC_KONST = ColorInput.CC_KONST
+CC_ZERO = ColorInput.CC_ZERO
+
+
+class AlphaInput(Enum):
+ CA_APREV = 0
+ CA_A0 = 1
+ CA_A1 = 2
+ CA_A2 = 3
+ CA_TEXA = 4
+ CA_RASA = 5
+ CA_KONST = 6
+ CA_ZERO = 7
+
+
+CA_APREV = AlphaInput.CA_APREV
+CA_A0 = AlphaInput.CA_A0
+CA_A1 = AlphaInput.CA_A1
+CA_A2 = AlphaInput.CA_A2
+CA_TEXA = AlphaInput.CA_TEXA
+CA_RASA = AlphaInput.CA_RASA
+CA_KONST = AlphaInput.CA_KONST
+CA_ZERO = AlphaInput.CA_ZERO
+
+
+class TevFunction(Enum):
+ TEV_ADD = 0
+ TEV_SUB = 1
+ TEV_COMP_R8_GT = 8
+ TEV_COMP_R8_EQ = 9
+ TEV_COMP_GR16_GT = 10
+ TEV_COMP_GR16_EQ = 11
+ TEV_COMP_BGR24_GT = 12
+ TEV_COMP_BGR24_EQ = 13
+ TEV_COMP_RGB8_GT = 14
+ TEV_COMP_RGB8_EQ = 15
+ TEV_COMP_A8_GT = TEV_COMP_RGB8_GT
+ TEV_COMP_A8_EQ = TEV_COMP_RGB8_EQ
+
+
+TEV_ADD = TevFunction.TEV_ADD
+TEV_SUB = TevFunction.TEV_SUB
+TEV_COMP_R8_GT = TevFunction.TEV_COMP_R8_GT
+TEV_COMP_R8_EQ = TevFunction.TEV_COMP_R8_EQ
+TEV_COMP_GR16_GT = TevFunction.TEV_COMP_GR16_GT
+TEV_COMP_GR16_EQ = TevFunction.TEV_COMP_GR16_EQ
+TEV_COMP_BGR24_GT = TevFunction.TEV_COMP_BGR24_GT
+TEV_COMP_BGR24_EQ = TevFunction.TEV_COMP_BGR24_EQ
+TEV_COMP_RGB8_GT = TevFunction.TEV_COMP_RGB8_GT
+TEV_COMP_RGB8_EQ = TevFunction.TEV_COMP_RGB8_EQ
+TEV_COMP_A8_GT = TevFunction.TEV_COMP_A8_GT
+TEV_COMP_A8_EQ = TevFunction.TEV_COMP_A8_EQ
+
+
+class TevBias(Enum):
+ TB_ZERO = 0
+ TB_ADDHALF = 1
+ TB_SUBHALF = 2
+ TB_UNKNOWN0 = 3
+
+
+TB_ZERO = TevBias.TB_ZERO
+TB_ADDHALF = TevBias.TB_ADDHALF
+TB_SUBHALF = TevBias.TB_SUBHALF
+TB_UNKNOWN0 = TevBias.TB_UNKNOWN0
+
+
+class TevScale(Enum):
+ CS_SCALE_1 = 0
+ CS_SCALE_2 = 1
+ CS_SCALE_4 = 2
+ CS_DIVIDE_2 = 3
+
+
+CS_SCALE_1 = TevScale.CS_SCALE_1
+CS_SCALE_2 = TevScale.CS_SCALE_2
+CS_SCALE_4 = TevScale.CS_SCALE_4
+CS_DIVIDE_2 = TevScale.CS_DIVIDE_2
+
+
+class TevColor(Enum):
+ TEVPREV = 0
+ TEVREG0 = Value(1,index=0)
+ TEVREG1 = Value(2,index=1)
+ TEVREG2 = Value(3,index=2)
+
+
+TEVPREV = TevColor.TEVPREV
+TEVREG0 = TevColor.TEVREG0
+TEVREG1 = TevColor.TEVREG1
+TEVREG2 = TevColor.TEVREG2
+TEVREG = [TEVREG0,TEVREG1,TEVREG2]
+
+
+class KColor(Enum):
+ KCOLOR0 = Value(0,index=0)
+ KCOLOR1 = Value(1,index=1)
+ KCOLOR2 = Value(2,index=2)
+ KCOLOR3 = Value(3,index=3)
+
+
+KCOLOR0 = KColor.KCOLOR0
+KCOLOR1 = KColor.KCOLOR1
+KCOLOR2 = KColor.KCOLOR2
+KCOLOR3 = KColor.KCOLOR3
+KCOLOR = [KCOLOR0,KCOLOR1,KCOLOR2,KCOLOR3]
+
+
+class ConstantColor(Enum):
+ TEV_KCSEL_1 = 0
+ TEV_KCSEL_7_8 = 1
+ TEV_KCSEL_3_4 = 2
+ TEV_KCSEL_5_8 = 3
+ TEV_KCSEL_1_2 = 4
+ TEV_KCSEL_3_8 = 5
+ TEV_KCSEL_1_4 = 6
+ TEV_KCSEL_1_8 = 7
+ TEV_KCSEL_K0 = 12
+ TEV_KCSEL_K1 = 13
+ TEV_KCSEL_K2 = 14
+ TEV_KCSEL_K3 = 15
+ TEV_KCSEL_K0_R = 16
+ TEV_KCSEL_K1_R = 17
+ TEV_KCSEL_K2_R = 18
+ TEV_KCSEL_K3_R = 19
+ TEV_KCSEL_K0_G = 20
+ TEV_KCSEL_K1_G = 21
+ TEV_KCSEL_K2_G = 22
+ TEV_KCSEL_K3_G = 23
+ TEV_KCSEL_K0_B = 24
+ TEV_KCSEL_K1_B = 25
+ TEV_KCSEL_K2_B = 26
+ TEV_KCSEL_K3_B = 27
+ TEV_KCSEL_K0_A = 28
+ TEV_KCSEL_K1_A = 29
+ TEV_KCSEL_K2_A = 30
+ TEV_KCSEL_K3_A = 31
+
+
+TEV_KCSEL_1 = ConstantColor.TEV_KCSEL_1
+TEV_KCSEL_7_8 = ConstantColor.TEV_KCSEL_7_8
+TEV_KCSEL_3_4 = ConstantColor.TEV_KCSEL_3_4
+TEV_KCSEL_5_8 = ConstantColor.TEV_KCSEL_5_8
+TEV_KCSEL_1_2 = ConstantColor.TEV_KCSEL_1_2
+TEV_KCSEL_3_8 = ConstantColor.TEV_KCSEL_3_8
+TEV_KCSEL_1_4 = ConstantColor.TEV_KCSEL_1_4
+TEV_KCSEL_1_8 = ConstantColor.TEV_KCSEL_1_8
+TEV_KCSEL_K0 = ConstantColor.TEV_KCSEL_K0
+TEV_KCSEL_K1 = ConstantColor.TEV_KCSEL_K1
+TEV_KCSEL_K2 = ConstantColor.TEV_KCSEL_K2
+TEV_KCSEL_K3 = ConstantColor.TEV_KCSEL_K3
+TEV_KCSEL_K0_R = ConstantColor.TEV_KCSEL_K0_R
+TEV_KCSEL_K1_R = ConstantColor.TEV_KCSEL_K1_R
+TEV_KCSEL_K2_R = ConstantColor.TEV_KCSEL_K2_R
+TEV_KCSEL_K3_R = ConstantColor.TEV_KCSEL_K3_R
+TEV_KCSEL_K0_G = ConstantColor.TEV_KCSEL_K0_G
+TEV_KCSEL_K1_G = ConstantColor.TEV_KCSEL_K1_G
+TEV_KCSEL_K2_G = ConstantColor.TEV_KCSEL_K2_G
+TEV_KCSEL_K3_G = ConstantColor.TEV_KCSEL_K3_G
+TEV_KCSEL_K0_B = ConstantColor.TEV_KCSEL_K0_B
+TEV_KCSEL_K1_B = ConstantColor.TEV_KCSEL_K1_B
+TEV_KCSEL_K2_B = ConstantColor.TEV_KCSEL_K2_B
+TEV_KCSEL_K3_B = ConstantColor.TEV_KCSEL_K3_B
+TEV_KCSEL_K0_A = ConstantColor.TEV_KCSEL_K0_A
+TEV_KCSEL_K1_A = ConstantColor.TEV_KCSEL_K1_A
+TEV_KCSEL_K2_A = ConstantColor.TEV_KCSEL_K2_A
+TEV_KCSEL_K3_A = ConstantColor.TEV_KCSEL_K3_A
+
+
+class ConstantAlpha(Enum):
+ TEV_KASEL_1 = 0
+ TEV_KASEL_7_8 = 1
+ TEV_KASEL_3_4 = 2
+ TEV_KASEL_5_8 = 3
+ TEV_KASEL_1_2 = 4
+ TEV_KASEL_3_8 = 5
+ TEV_KASEL_1_4 = 6
+ TEV_KASEL_1_8 = 7
+ TEV_KASEL_K0_R = 16
+ TEV_KASEL_K1_R = 17
+ TEV_KASEL_K2_R = 18
+ TEV_KASEL_K3_R = 19
+ TEV_KASEL_K0_G = 20
+ TEV_KASEL_K1_G = 21
+ TEV_KASEL_K2_G = 22
+ TEV_KASEL_K3_G = 23
+ TEV_KASEL_K0_B = 24
+ TEV_KASEL_K1_B = 25
+ TEV_KASEL_K2_B = 26
+ TEV_KASEL_K3_B = 27
+ TEV_KASEL_K0_A = 28
+ TEV_KASEL_K1_A = 29
+ TEV_KASEL_K2_A = 30
+ TEV_KASEL_K3_A = 31
+
+
+TEV_KASEL_1 = ConstantAlpha.TEV_KASEL_1
+TEV_KASEL_7_8 = ConstantAlpha.TEV_KASEL_7_8
+TEV_KASEL_3_4 = ConstantAlpha.TEV_KASEL_3_4
+TEV_KASEL_5_8 = ConstantAlpha.TEV_KASEL_5_8
+TEV_KASEL_1_2 = ConstantAlpha.TEV_KASEL_1_2
+TEV_KASEL_3_8 = ConstantAlpha.TEV_KASEL_3_8
+TEV_KASEL_1_4 = ConstantAlpha.TEV_KASEL_1_4
+TEV_KASEL_1_8 = ConstantAlpha.TEV_KASEL_1_8
+TEV_KASEL_K0_R = ConstantAlpha.TEV_KASEL_K0_R
+TEV_KASEL_K1_R = ConstantAlpha.TEV_KASEL_K1_R
+TEV_KASEL_K2_R = ConstantAlpha.TEV_KASEL_K2_R
+TEV_KASEL_K3_R = ConstantAlpha.TEV_KASEL_K3_R
+TEV_KASEL_K0_G = ConstantAlpha.TEV_KASEL_K0_G
+TEV_KASEL_K1_G = ConstantAlpha.TEV_KASEL_K1_G
+TEV_KASEL_K2_G = ConstantAlpha.TEV_KASEL_K2_G
+TEV_KASEL_K3_G = ConstantAlpha.TEV_KASEL_K3_G
+TEV_KASEL_K0_B = ConstantAlpha.TEV_KASEL_K0_B
+TEV_KASEL_K1_B = ConstantAlpha.TEV_KASEL_K1_B
+TEV_KASEL_K2_B = ConstantAlpha.TEV_KASEL_K2_B
+TEV_KASEL_K3_B = ConstantAlpha.TEV_KASEL_K3_B
+TEV_KASEL_K0_A = ConstantAlpha.TEV_KASEL_K0_A
+TEV_KASEL_K1_A = ConstantAlpha.TEV_KASEL_K1_A
+TEV_KASEL_K2_A = ConstantAlpha.TEV_KASEL_K2_A
+TEV_KASEL_K3_A = ConstantAlpha.TEV_KASEL_K3_A
+
+
+class SwapTable(Enum):
+ TEV_SWAP0 = Value(0,index=0)
+ TEV_SWAP1 = Value(1,index=1)
+ TEV_SWAP2 = Value(2,index=2)
+ TEV_SWAP3 = Value(3,index=3)
+
+
+TEV_SWAP0 = SwapTable.TEV_SWAP0
+TEV_SWAP1 = SwapTable.TEV_SWAP1
+TEV_SWAP2 = SwapTable.TEV_SWAP2
+TEV_SWAP3 = SwapTable.TEV_SWAP3
+TEV_SWAP = [TEV_SWAP0,TEV_SWAP1,TEV_SWAP2,TEV_SWAP3]
+
+
+class ColorComponent(Enum):
+ CH_RED = 0
+ CH_GREEN = 1
+ CH_BLUE = 2
+ CH_ALPHA = 3
+
+
+CH_RED = ColorComponent.CH_RED
+CH_GREEN = ColorComponent.CH_GREEN
+CH_BLUE = ColorComponent.CH_BLUE
+CH_ALPHA = ColorComponent.CH_ALPHA
+
+
+class IndirectStage(Enum):
+ INDTEXSTAGE0 = Value(0,index=0)
+ INDTEXSTAGE1 = Value(1,index=1)
+ INDTEXSTAGE2 = Value(2,index=2)
+ INDTEXSTAGE3 = Value(3,index=3)
+
+
+INDTEXSTAGE0 = IndirectStage.INDTEXSTAGE0
+INDTEXSTAGE1 = IndirectStage.INDTEXSTAGE1
+INDTEXSTAGE2 = IndirectStage.INDTEXSTAGE2
+INDTEXSTAGE3 = IndirectStage.INDTEXSTAGE3
+INDTEXSTAGE = [INDTEXSTAGE0,INDTEXSTAGE1,INDTEXSTAGE2,INDTEXSTAGE3]
+
+
+class IndirectFormat(Enum):
+ ITF_8 = 0
+ ITF_5 = 1
+ ITF_4 = 2
+ ITF_3 = 3
+
+
+ITF_8 = IndirectFormat.ITF_8
+ITF_5 = IndirectFormat.ITF_5
+ITF_4 = IndirectFormat.ITF_4
+ITF_3 = IndirectFormat.ITF_3
+
+
+class IndirectBiasComponents(Enum):
+ ITB_NONE = 0
+ ITB_S = 1
+ ITB_T = 2
+ ITB_ST = 3
+ ITB_U = 4
+ ITB_SU = 5
+ ITB_TU = 6
+ ITB_STU = 7
+
+
+ITB_NONE = IndirectBiasComponents.ITB_NONE
+ITB_S = IndirectBiasComponents.ITB_S
+ITB_T = IndirectBiasComponents.ITB_T
+ITB_ST = IndirectBiasComponents.ITB_ST
+ITB_U = IndirectBiasComponents.ITB_U
+ITB_SU = IndirectBiasComponents.ITB_SU
+ITB_TU = IndirectBiasComponents.ITB_TU
+ITB_STU = IndirectBiasComponents.ITB_STU
+
+
+class IndirectMatrix(Enum):
+ ITM_OFF = 0
+ ITM_0 = Value(1,index=0)
+ ITM_1 = Value(2,index=1)
+ ITM_2 = Value(3,index=2)
+ ITM_S0 = Value(5,index=0)
+ ITM_S1 = Value(6,index=1)
+ ITM_S2 = Value(7,index=2)
+ ITM_T0 = Value(9,index=0)
+ ITM_T1 = Value(10,index=1)
+ ITM_T2 = Value(11,index=2)
+
+
+ITM_OFF = IndirectMatrix.ITM_OFF
+ITM_0 = IndirectMatrix.ITM_0
+ITM_1 = IndirectMatrix.ITM_1
+ITM_2 = IndirectMatrix.ITM_2
+ITM = [ITM_0,ITM_1,ITM_2]
+ITM_S0 = IndirectMatrix.ITM_S0
+ITM_S1 = IndirectMatrix.ITM_S1
+ITM_S2 = IndirectMatrix.ITM_S2
+ITM_S = [ITM_S0,ITM_S1,ITM_S2]
+ITM_T0 = IndirectMatrix.ITM_T0
+ITM_T1 = IndirectMatrix.ITM_T1
+ITM_T2 = IndirectMatrix.ITM_T2
+ITM_T = [ITM_T0,ITM_T1,ITM_T2]
+
+
+class IndirectWrap(Enum):
+ ITW_OFF = 0
+ ITW_256 = 1
+ ITW_128 = 2
+ ITW_64 = 3
+ ITW_32 = 4
+ ITW_16 = 5
+ ITW_0 = 6
+
+
+ITW_OFF = IndirectWrap.ITW_OFF
+ITW_256 = IndirectWrap.ITW_256
+ITW_128 = IndirectWrap.ITW_128
+ITW_64 = IndirectWrap.ITW_64
+ITW_32 = IndirectWrap.ITW_32
+ITW_16 = IndirectWrap.ITW_16
+ITW_0 = IndirectWrap.ITW_0
+
+
+class IndirectBumpAlpha(Enum):
+ ITBA_OFF = 0
+ ITBA_S = 1
+ ITBA_T = 2
+ ITBA_U = 3
+
+
+ITBA_OFF = IndirectBumpAlpha.ITBA_OFF
+ITBA_S = IndirectBumpAlpha.ITBA_S
+ITBA_T = IndirectBumpAlpha.ITBA_T
+ITBA_U = IndirectBumpAlpha.ITBA_U
+
+
+class IndirectScale(Enum):
+ ITS_1 = 0
+ ITS_2 = 1
+ ITS_4 = 2
+ ITS_8 = 3
+ ITS_16 = 4
+ ITS_32 = 5
+ ITS_64 = 6
+ ITS_128 = 7
+ ITS_256 = 8
+
+
+ITS_1 = IndirectScale.ITS_1
+ITS_2 = IndirectScale.ITS_2
+ITS_4 = IndirectScale.ITS_4
+ITS_8 = IndirectScale.ITS_8
+ITS_16 = IndirectScale.ITS_16
+ITS_32 = IndirectScale.ITS_32
+ITS_64 = IndirectScale.ITS_64
+ITS_128 = IndirectScale.ITS_128
+ITS_256 = IndirectScale.ITS_256
+
+
+class CullMode(Enum):
+ CULL_NONE = 0
+ CULL_FRONT = Value(1,gl_value=GL_FRONT)
+ CULL_BACK = Value(2,gl_value=GL_BACK)
+ CULL_ALL = Value(3,gl_value=GL_FRONT_AND_BACK)
+
+
+CULL_NONE = CullMode.CULL_NONE
+CULL_FRONT = CullMode.CULL_FRONT
+CULL_BACK = CullMode.CULL_BACK
+CULL_ALL = CullMode.CULL_ALL
+
+
+class FogFunction(Enum):
+ FOG_NONE = 0
+ FOG_PERSP_LIN = 2
+ FOG_PERSP_EXP = 4
+ FOG_PERSP_EXP2 = 5
+ FOG_PERSP_REVEXP = 6
+ FOG_PERSP_REVEXP2 = 7
+ FOG_ORTHO_LIN = 10
+ FOG_ORTHO_EXP = 12
+ FOG_ORTHO_EXP2 = 13
+ FOG_ORTHO_REVEXP = 14
+ FOG_ORTHO_REVEXP2 = 15
+ FOG_LIN = FOG_PERSP_LIN
+ FOG_EXP = FOG_PERSP_EXP
+ FOG_EXP2 = FOG_PERSP_EXP2
+ FOG_REVEXP = FOG_PERSP_REVEXP
+ FOG_REVEXP2 = FOG_PERSP_REVEXP2
+
+
+FOG_NONE = FogFunction.FOG_NONE
+FOG_PERSP_LIN = FogFunction.FOG_PERSP_LIN
+FOG_PERSP_EXP = FogFunction.FOG_PERSP_EXP
+FOG_PERSP_EXP2 = FogFunction.FOG_PERSP_EXP2
+FOG_PERSP_REVEXP = FogFunction.FOG_REVEXP
+FOG_PERSP_REVEXP2 = FogFunction.FOG_REVEXP2
+FOG_ORTHO_LIN = FogFunction.FOG_ORTHO_LIN
+FOG_ORTHO_EXP = FogFunction.FOG_ORTHO_EXP
+FOG_ORTHO_EXP2 = FogFunction.FOG_ORTHO_EXP2
+FOG_ORTHO_REVEXP = FogFunction.FOG_ORTHO_REVEXP
+FOG_ORTHO_REVEXP2 = FogFunction.FOG_ORTHO_REVEXP2
+FOG_LIN = FogFunction.FOG_LIN
+FOG_EXP = FogFunction.FOG_EXP
+FOG_EXP2 = FogFunction.FOG_EXP2
+FOG_REVEXP = FogFunction.FOG_REVEXP
+FOG_REVEXP2 = FogFunction.FOG_REVEXP2
+
+
+class AlphaOperator(Enum):
+ AOP_AND = 0
+ AOP_OR = 1
+ AOP_XOR = 2
+ AOP_XNOR = 3
+
+
+AOP_AND = AlphaOperator.AOP_AND
+AOP_OR = AlphaOperator.AOP_OR
+AOP_XOR = AlphaOperator.AOP_XOR
+AOP_XNOR = AlphaOperator.AOP_XNOR
+
+
+class CompareFunction(Enum):
+ NEVER = Value(0,gl_value=GL_NEVER)
+ LESS = Value(1,gl_value=GL_LESS)
+ EQUAL = Value(2,gl_value=GL_EQUAL)
+ LEQUAL = Value(3,gl_value=GL_LEQUAL)
+ GREATER = Value(4,gl_value=GL_GREATER)
+ NEQUAL = Value(5,gl_value=GL_NOTEQUAL)
+ GEQUAL = Value(6,gl_value=GL_GEQUAL)
+ ALWAYS = Value(7,gl_value=GL_ALWAYS)
+
+
+NEVER = CompareFunction.NEVER
+LESS = CompareFunction.LESS
+EQUAL = CompareFunction.EQUAL
+LEQUAL = CompareFunction.LEQUAL
+GREATER = CompareFunction.GREATER
+NEQUAL = CompareFunction.NEQUAL
+GEQUAL = CompareFunction.GEQUAL
+ALWAYS = CompareFunction.ALWAYS
+
+
+class BlendFunction(Enum):
+ BM_NONE = 0
+ BM_BLEND = 1
+ BM_LOGIC = 2
+ BM_SUBTRACT = 3
+
+
+BM_NONE = BlendFunction.BM_NONE
+BM_BLEND = BlendFunction.BM_BLEND
+BM_LOGIC = BlendFunction.BM_LOGIC
+BM_SUBTRACT = BlendFunction.BM_SUBTRACT
+
+
+class BlendFactor(Enum):
+ BL_ZERO = Value(0,gl_value=GL_ZERO)
+ BL_ONE = Value(1,gl_value=GL_ONE)
+ BL_SRCALPHA = Value(4,gl_value=GL_SRC_ALPHA)
+ BL_INVSRCALPHA = Value(5,gl_value=GL_ONE_MINUS_SRC_ALPHA)
+ BL_DSTALPHA = Value(6,gl_value=GL_DST_ALPHA)
+ BL_INVDSTALPHA = Value(7,gl_value=GL_ONE_MINUS_DST_ALPHA)
+
+
+class BlendSourceFactor(BlendFactor,metaclass=ExtendEnumMeta):
+ BL_DSTCLR = Value(2,gl_value=GL_DST_COLOR)
+ BL_INVDSTCLR = Value(3,gl_value=GL_ONE_MINUS_DST_COLOR)
+
+
+class BlendDestinationFactor(BlendFactor,metaclass=ExtendEnumMeta):
+ BL_SRCCLR = Value(2,gl_value=GL_SRC_COLOR)
+ BL_INVSRCCLR = Value(3,gl_value=GL_ONE_MINUS_SRC_COLOR)
+
+
+BL_ZERO = BlendFactor.BL_ZERO
+BL_ONE = BlendFactor.BL_ONE
+BL_SRCCLR = BlendDestinationFactor.BL_SRCCLR
+BL_INVSRCCLR = BlendDestinationFactor.BL_INVSRCCLR
+BL_DSTCLR = BlendSourceFactor.BL_DSTCLR
+BL_INVDSTCLR = BlendSourceFactor.BL_INVDSTCLR
+BL_SRCALPHA = BlendFactor.BL_SRCALPHA
+BL_INVSRCALPHA = BlendFactor.BL_INVSRCALPHA
+BL_DSTALPHA = BlendFactor.BL_DSTALPHA
+BL_INVDSTALPHA = BlendFactor.BL_INVDSTALPHA
+
+
+class LogicalOperation(Enum):
+ LO_CLEAR = Value(0,gl_value=GL_CLEAR)
+ LO_AND = Value(1,gl_value=GL_AND)
+ LO_REVAND = Value(2,gl_value=GL_AND_REVERSE)
+ LO_COPY = Value(3,gl_value=GL_COPY)
+ LO_INVAND = Value(4,gl_value=GL_AND_INVERTED)
+ LO_NOOP = Value(5,gl_value=GL_NOOP)
+ LO_XOR = Value(6,gl_value=GL_XOR)
+ LO_OR = Value(7,gl_value=GL_OR)
+ LO_NOR = Value(8,gl_value=GL_NOR)
+ LO_EQUIV = Value(9,gl_value=GL_EQUIV)
+ LO_INV = Value(10,gl_value=GL_INVERT)
+ LO_REVOR = Value(11,gl_value=GL_OR_INVERTED)
+ LO_INVCOPY = Value(12,gl_value=GL_COPY_INVERTED)
+ LO_INVOR = Value(13,gl_value=GL_OR_INVERTED)
+ LO_INVNAND = Value(14,gl_value=GL_NAND)
+ LO_SET = Value(15,gl_value=GL_SET)
+
+
+LO_CLEAR = LogicalOperation.LO_CLEAR
+LO_AND = LogicalOperation.LO_AND
+LO_REVAND = LogicalOperation.LO_REVAND
+LO_COPY = LogicalOperation.LO_COPY
+LO_INVAND = LogicalOperation.LO_INVAND
+LO_NOOP = LogicalOperation.LO_NOOP
+LO_XOR = LogicalOperation.LO_XOR
+LO_OR = LogicalOperation.LO_OR
+LO_NOR = LogicalOperation.LO_NOR
+LO_EQUIV = LogicalOperation.LO_EQUIV
+LO_INV = LogicalOperation.LO_INV
+LO_REVOR = LogicalOperation.LO_REVOR
+LO_INVCOPY = LogicalOperation.LO_INVCOPY
+LO_INVOR = LogicalOperation.LO_INVOR
+LO_INVNAND = LogicalOperation.LO_INVAND
+LO_SET = LogicalOperation.LO_SET
+
+
+class Aniso(Enum):
+ ANISO_1 = 0
+ ANISO_2 = 1
+ ANISO_3 = 2
+
+
+ANISO_1 = Aniso.ANISO_1
+ANISO_2 = Aniso.ANISO_2
+ANISO_3 = Aniso.ANISO_3
+
diff --git a/gx/texture.pyx b/gx/texture.pyx
new file mode 100644
index 0000000..733d8fc
--- /dev/null
+++ b/gx/texture.pyx
@@ -0,0 +1,603 @@
+#cython: boundscheck=False, wraparound=False, cdivision=True
+
+import numpy
+cimport numpy
+from OpenGL.GL import *
+import gl
+import gx
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+cdef numpy.uint16_t swap_bytes_uint16(numpy.uint16_t i):
+ return (i << 8) | (i >> 8)
+
+
+cdef numpy.uint32_t swap_bytes_uint32(numpy.uint32_t i):
+ return (i << 24) | ((i << 8) & 0xFF0000) | ((i >> 8) & 0xFF00) | (i >> 24)
+
+
+def native_byteorder(array):
+ return array.view(array.dtype.newbyteorder('<'))
+
+
+cdef void rgb5a3_to_rgba8(numpy.uint16_t source,numpy.uint8_t[:] destination):
+ source = swap_bytes_uint16(source)
+ if source & 0x8000:
+ destination[0] = ((source >> 7) & 0xF8) | ((source >> 12) & 0x7)
+ destination[1] = ((source >> 2) & 0xF8) | ((source >> 7) & 0x7)
+ destination[2] = ((source << 3) & 0xF8) | ((source >> 2) & 0x7)
+ destination[3] = 0xFF
+ else:
+ destination[0] = ((source >> 4) & 0xF0) | ((source >> 8) & 0xF)
+ destination[1] = (source & 0xF0) | ((source >> 4) & 0xF)
+ destination[2] = ((source << 4) & 0xF0) | (source & 0xF)
+ destination[3] = ((source >> 7) & 0xE0) | ((source >> 10) & 0x1C) | ((source >> 13) & 0x3)
+
+
+def untile(source,destination):
+ height = destination.shape[0]
+ width = destination.shape[1]
+ tile_height = source.shape[2]
+ tile_width = source.shape[3]
+
+ for i in range(min(tile_height,height)):
+ for j in range(min(tile_width,width)):
+ d = destination[i::tile_height,j::tile_width]
+ d[:] = source[:d.shape[0],:d.shape[1],i,j]
+
+
+dxt1_block = numpy.dtype([('color0',numpy.uint16),('color1',numpy.uint16),('indices',numpy.uint32)])
+
+
+cdef packed struct dxt1_block_t:
+ numpy.uint16_t color0
+ numpy.uint16_t color1
+ numpy.uint32_t indices
+
+
+cdef void dxt1_decompress_block(dxt1_block_t block,numpy.uint8_t[:,:,:] destination):
+ cdef numpy.uint16_t color0 = swap_bytes_uint16(block.color0)
+ cdef numpy.uint16_t color1 = swap_bytes_uint16(block.color1)
+ cdef numpy.uint32_t indices = swap_bytes_uint32(block.indices)
+ cdef numpy.uint8_t color_table[4][4]
+ cdef unsigned int i,j,index
+
+ color_table[0][0] = ((color0 >> 8) & 0xF8) | ((color0 >> 11) & 0x7)
+ color_table[0][1] = ((color0 >> 3) & 0xFC) | ((color0 >> 5) & 0x3)
+ color_table[0][2] = ((color0 << 3) & 0xF8) | (color0 & 0x7)
+ color_table[0][3] = 0xFF
+ color_table[1][0] = ((color1 >> 8) & 0xF8) | ((color1 >> 11) & 0x7)
+ color_table[1][1] = ((color1 >> 3) & 0xFC) | ((color1 >> 5) & 0x3)
+ color_table[1][2] = ((color1 << 3) & 0xF8) | (color1 & 0x7)
+ color_table[1][3] = 0xFF
+
+ if color0 > color1:
+ color_table[2][0] = (2*color_table[0][0] + color_table[1][0])//3
+ color_table[2][1] = (2*color_table[0][1] + color_table[1][1])//3
+ color_table[2][2] = (2*color_table[0][2] + color_table[1][2])//3
+ color_table[2][3] = 0xFF
+ color_table[3][0] = (2*color_table[1][0] + color_table[0][0])//3
+ color_table[3][1] = (2*color_table[1][1] + color_table[0][1])//3
+ color_table[3][2] = (2*color_table[1][2] + color_table[0][2])//3
+ color_table[3][3] = 0xFF
+ else:
+ color_table[2][0] = (color_table[0][0] + color_table[1][0])//2
+ color_table[2][1] = (color_table[0][1] + color_table[1][1])//2
+ color_table[2][2] = (color_table[0][2] + color_table[1][2])//2
+ color_table[2][3] = 0xFF
+ color_table[3][0] = (2*color_table[1][0] + color_table[0][0])//3
+ color_table[3][1] = (2*color_table[1][1] + color_table[0][1])//3
+ color_table[3][2] = (2*color_table[1][2] + color_table[0][2])//3
+ color_table[3][3] = 0
+
+ for i in range(destination.shape[0]):
+ for j in range(destination.shape[1]):
+ index = (indices >> (30 - 2*(4*i + j))) & 0x3
+ destination[i,j,0] = color_table[index][0]
+ destination[i,j,1] = color_table[index][1]
+ destination[i,j,2] = color_table[index][2]
+ destination[i,j,3] = color_table[index][3]
+
+
+class HashableArray(numpy.ndarray):
+
+ def __hash__(self):
+ return object.__hash__(self)
+
+
+class PaletteIA8(HashableArray):
+ palette_format = gx.TL_IA8
+ entry_type = numpy.dtype((numpy.uint8,2))
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RG
+ gl_texel_type = numpy.dtype((numpy.uint8,2))
+ gl_swizzle = numpy.array([GL_RED,GL_RED,GL_RED,GL_GREEN],numpy.int32)
+
+
+class PaletteRGB565(HashableArray):
+ palette_format = gx.TL_RGB565
+ entry_type = numpy.dtype(numpy.uint16).newbyteorder('>')
+ gl_image_format = GL_UNSIGNED_SHORT_5_6_5
+ gl_component_count = GL_RGB
+ gl_texel_type = numpy.uint16
+ gl_swizzle = numpy.array([GL_RED,GL_GREEN,GL_BLUE,GL_ONE],numpy.int32)
+
+
+class PaletteRGB5A3(HashableArray):
+ palette_format = gx.TL_RGB5A3
+ entry_type = numpy.dtype(numpy.uint16).newbyteorder('>')
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RGBA
+ gl_texel_type = numpy.dtype((numpy.uint8,4))
+ gl_swizzle = numpy.array([GL_RED,GL_GREEN,GL_BLUE,GL_ALPHA],numpy.int32)
+
+
+class ImageI4(HashableArray):
+ image_format = gx.TF_I4
+ tile_width = 8
+ tile_height = 8
+ tile_type = numpy.dtype((numpy.uint8,(8,4)))
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RED
+ gl_swizzle = numpy.array([GL_RED,GL_RED,GL_RED,GL_RED],numpy.int32)
+
+ def gl_convert(self,palette):
+ cdef unsigned int width = self.width
+ cdef unsigned int height = self.height
+ cdef numpy.uint8_t[:,:,:,:] self_view = self
+ cdef numpy.ndarray[numpy.uint8_t,ndim=2] image = numpy.empty((height,width),numpy.uint8)
+ cdef unsigned int i,j,texel
+
+ for i in range(height):
+ for j in range(0,width,2):
+ texel = self_view[i//8,j//8,i % 8,(j//2) % 4]
+ image[i,j] = (texel & 0xF0) | ((texel >> 4) & 0xF)
+ if j + 1 >= width: break
+ image[i,j + 1] = ((texel << 4) & 0xF0) | (texel & 0xF)
+
+ return image
+
+
+class ImageI8(HashableArray):
+ image_format = gx.TF_I8
+ tile_width = 8
+ tile_height = 4
+ tile_type = numpy.dtype((numpy.uint8,(4,8)))
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RED
+ gl_swizzle = numpy.array([GL_RED,GL_RED,GL_RED,GL_RED],numpy.int32)
+
+ def gl_convert(self,palette):
+ image = numpy.empty((self.height,self.width),numpy.uint8)
+ untile(self,image)
+ return image
+
+
+class ImageIA4(HashableArray):
+ image_format = gx.TF_IA4
+ tile_width = 8
+ tile_height = 4
+ tile_type = numpy.dtype((numpy.uint8,(4,8)))
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RG
+ gl_swizzle = numpy.array([GL_RED,GL_RED,GL_RED,GL_GREEN],numpy.int32)
+
+ def gl_convert(self,palette):
+ cdef unsigned int width = self.width
+ cdef unsigned int height = self.height
+ cdef numpy.uint8_t[:,:,:,:] self_view = self
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,2),numpy.uint8)
+ cdef unsigned int i,j,texel
+
+ for i in range(height):
+ for j in range(width):
+ texel = self_view[i//4,j//8,i % 4,j % 8]
+ image[i,j,0] = ((texel << 4) & 0xF0) | (texel & 0xF)
+ image[i,j,1] = (texel & 0xF0) | ((texel >> 4) & 0xF)
+
+ return image
+
+
+class ImageIA8(HashableArray):
+ image_format = gx.TF_IA8
+ tile_width = 4
+ tile_height = 4
+ tile_type = numpy.dtype((numpy.uint8,(4,4,2)))
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RG
+ gl_swizzle = numpy.array([GL_RED,GL_RED,GL_RED,GL_GREEN],numpy.int32)
+
+ def gl_convert(self,palette):
+ image = numpy.empty((self.height,self.width,2),numpy.uint8)
+ untile(self[:,:,:,:,0],image[:,:,1])
+ untile(self[:,:,:,:,1],image[:,:,0])
+ return image
+
+
+class ImageRGB565(HashableArray):
+ image_format = gx.TF_RGB565
+ tile_width = 4
+ tile_height = 4
+ tile_type = numpy.dtype((numpy.uint16,(4,4))).newbyteorder('>')
+ gl_image_format = GL_UNSIGNED_SHORT_5_6_5
+ gl_component_count = GL_RGB
+ gl_swizzle = numpy.array([GL_RED,GL_GREEN,GL_BLUE,GL_ONE],numpy.int32)
+
+ def gl_convert(self,palette):
+ image = numpy.empty((self.height,self.width),numpy.uint16)
+ untile(self,image)
+ return image
+
+
+class ImageRGB5A3(HashableArray):
+ image_format = gx.TF_RGB5A3
+ tile_width = 4
+ tile_height = 4
+ tile_type = numpy.dtype((numpy.uint16,(4,4))).newbyteorder('>')
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RGBA
+ gl_swizzle = numpy.array([GL_RED,GL_GREEN,GL_BLUE,GL_ALPHA],numpy.int32)
+
+ def gl_convert(self,palette):
+ cdef unsigned int width = self.width
+ cdef unsigned int height = self.height
+ cdef numpy.uint16_t[:,:,:,:] self_view = native_byteorder(self)
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,4),numpy.uint8)
+ cdef unsigned int i,j,texel
+
+ for i in range(height):
+ for j in range(width):
+ rgb5a3_to_rgba8(self_view[i//4,j//4,i % 4,j % 4],image[i,j])
+
+ return image
+
+
+class ImageRGBA8(HashableArray):
+ image_format = gx.TF_RGBA8
+ tile_width = 4
+ tile_height = 4
+ tile_type = numpy.dtype((((numpy.uint8,2),(4,4)),2))
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RGBA
+ gl_swizzle = numpy.array([GL_RED,GL_GREEN,GL_BLUE,GL_ALPHA],numpy.int32)
+
+ def gl_convert(self,palette):
+ image = numpy.empty((self.height,self.width,4),numpy.uint8)
+ untile(self[:,:,0,:,:,0],image[:,:,3])
+ untile(self[:,:,0,:,:,1],image[:,:,0])
+ untile(self[:,:,1,:,:,0],image[:,:,1])
+ untile(self[:,:,1,:,:,1],image[:,:,2])
+ return image
+
+
+class ImageCMPR(HashableArray):
+ image_format = gx.TF_CMPR
+ tile_width = 8
+ tile_height = 8
+ tile_type = numpy.dtype((dxt1_block,(2,2))).newbyteorder('>')
+ gl_image_format = GL_UNSIGNED_BYTE
+ gl_component_count = GL_RGBA
+ gl_swizzle = numpy.array([GL_RED,GL_GREEN,GL_BLUE,GL_ALPHA],numpy.int32)
+
+ def gl_convert(self,palette):
+ cdef unsigned int width = self.width
+ cdef unsigned int height = self.height
+ cdef dxt1_block_t[:,:,:,:] self_view = native_byteorder(self)
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,4),numpy.uint8)
+ cdef numpy.uint8_t[:,:,:] image_view = image
+ cdef dxt1_block_t block
+ cdef unsigned int i,j
+
+ for i in range(0,height,4):
+ for j in range(0,width,4):
+ block = self_view[i//8,j//8,(i//4) % 2,(j//4) % 2]
+ dxt1_decompress_block(block,image_view[i:min(i + 4,height),j:min(j + 4,width)])
+
+ return image
+
+
+cdef gl_convert_ci4_ia8(numpy.uint8_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint8_t[:,:] palette):
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,2),numpy.uint8)
+ cdef unsigned int i,j,texel
+
+ for i in range(height):
+ for j in range(width):
+ texel = self_view[i//8,j//8,i % 8,(j//2) % 4]
+ image[i,j] = palette[(texel >> 4) & 0xF]
+ if j + 1 >= width: break
+ image[i,j + 1] = palette[texel & 0xF]
+
+ return image
+
+
+cdef gl_convert_ci4_rgb565(numpy.uint8_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint16_t[:] palette):
+ cdef numpy.ndarray[numpy.uint16_t,ndim=2] image = numpy.empty((height,width),numpy.uint16)
+ cdef unsigned int i,j,texel
+
+ for i in range(height):
+ for j in range(width):
+ texel = self_view[i//8,j//8,i % 8,(j//2) % 4]
+ image[i,j] = swap_bytes_uint16(palette[(texel >> 4) & 0xF])
+ if j + 1 >= width: break
+ image[i,j + 1] = swap_bytes_uint16(palette[texel & 0xF])
+
+ return image
+
+
+cdef gl_convert_ci4_rgb5a3(numpy.uint8_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint16_t[:] palette):
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,4),numpy.uint8)
+ cdef unsigned int i,j,texel
+
+ for i in range(height):
+ for j in range(width):
+ texel = self_view[i//8,j//8,i % 8,(j//2) % 4]
+ rgb5a3_to_rgba8(palette[(texel >> 4) & 0xF],image[i,j])
+ if j + 1 >= width: break
+ rgb5a3_to_rgba8(palette[texel & 0xF],image[i,j + 1])
+
+ return image
+
+
+cdef gl_convert_ci8_ia8(numpy.uint8_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint8_t[:,:] palette):
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,2),numpy.uint8)
+ cdef unsigned int i,j
+
+ for i in range(height):
+ for j in range(width):
+ image[i,j] = palette[self_view[i//4,j//4,i % 4,j % 4]]
+
+ return image
+
+
+cdef gl_convert_ci8_rgb565(numpy.uint8_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint16_t[:] palette):
+ cdef numpy.ndarray[numpy.uint16_t,ndim=2] image = numpy.empty((height,width),numpy.uint16)
+ cdef unsigned int i,j
+
+ for i in range(height):
+ for j in range(width):
+ image[i,j] = swap_bytes_uint16(palette[self_view[i//4,j//4,i % 4,j % 4]])
+
+ return image
+
+
+cdef gl_convert_ci8_rgb5a3(numpy.uint8_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint16_t[:] palette):
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,4),numpy.uint8)
+ cdef unsigned int i,j,texel
+
+ for i in range(height):
+ for j in range(width):
+ rgb5a3_to_rgba8(palette[self_view[i//4,j//4,i % 4,j % 4]],image[i,j])
+
+ return image
+
+
+cdef gl_convert_ci14_ia8(numpy.uint16_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint8_t[:,:] palette):
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,2),numpy.uint8)
+ cdef unsigned int i,j
+
+ for i in range(height):
+ for j in range(width):
+ image[i,j] = palette[swap_bytes_uint16(self_view[i//4,j//8,i % 4,j % 8]) & 0x3FFF]
+
+ return image
+
+
+cdef gl_convert_ci14_rgb565(numpy.uint16_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint16_t[:] palette):
+ cdef numpy.ndarray[numpy.uint16_t,ndim=2] image = numpy.empty((height,width),numpy.uint16)
+ cdef unsigned int i,j
+
+ for i in range(height):
+ for j in range(width):
+ image[i,j] = swap_bytes_uint16(palette[swap_bytes_uint16(self_view[i//4,j//8,i % 4,j % 8]) & 0x3FFF])
+
+ return image
+
+
+cdef gl_convert_ci14_rgb5a3(numpy.uint16_t[:,:,:,:] self_view,unsigned int width,unsigned int height,numpy.uint16_t[:] palette):
+ cdef numpy.ndarray[numpy.uint8_t,ndim=3] image = numpy.empty((height,width,4),numpy.uint8)
+ cdef unsigned int i,j
+
+ for i in range(height):
+ for j in range(width):
+ rgb5a3_to_rgba8(palette[swap_bytes_uint16(self_view[i//4,j//8,i % 4,j % 8]) & 0x3FFF],image[i,j])
+
+ return image
+
+
+class ImageCI4(HashableArray):
+ image_format = gx.TF_CI4
+ tile_width = 8
+ tile_height = 8
+ tile_type = numpy.dtype((numpy.uint8,(8,4)))
+
+ def gl_convert(self,palette):
+ if palette.palette_format == gx.TL_IA8:
+ return gl_convert_ci4_ia8(self,self.width,self.height,palette)
+ if palette.palette_format == gx.TL_RGB565:
+ return gl_convert_ci4_rgb565(self,self.width,self.height,native_byteorder(palette))
+ if palette.palette_format == gx.TL_RGB5A3:
+ return gl_convert_ci4_rgb5a3(self,self.width,self.height,native_byteorder(palette))
+
+ raise ValueError('invalid palette format')
+
+
+class ImageCI8(HashableArray):
+ image_format = gx.TF_CI8
+ tile_width = 8
+ tile_height = 4
+ tile_type = numpy.dtype((numpy.uint8,(4,8)))
+
+ def gl_convert(self,palette):
+ if palette.palette_format == gx.TL_IA8:
+ return gl_convert_ci8_ia8(self,self.width,self.height,palette)
+ if palette.palette_format == gx.TL_RGB565:
+ return gl_convert_ci8_rgb565(self,self.width,self.height,native_byteorder(palette))
+ if palette.palette_format == gx.TL_RGB5A3:
+ return gl_convert_ci8_rgb5a3(self,self.width,self.height,native_byteorder(palette))
+
+ raise ValueError('invalid palette format')
+
+
+class ImageCI14(HashableArray):
+ image_format = gx.TF_CI14
+ tile_width = 4
+ tile_height = 4
+ tile_type = numpy.dtype((numpy.uint16,(4,4))).newbyteorder('>')
+
+ def gl_convert(self,palette):
+ if palette.palette_format == gx.TL_IA8:
+ return gl_convert_ci14_ia8(native_byteorder(self),self.width,self.height,palette)
+ if palette.palette_format == gx.TL_RGB565:
+ return gl_convert_ci14_rgb565(native_byteorder(self),self.width,self.height,native_byteorder(palette))
+ if palette.palette_format == gx.TL_RGB5A3:
+ return gl_convert_ci14_rgb5a3(native_byteorder(self),self.width,self.height,native_byteorder(palette))
+
+ raise ValueError('invalid palette format')
+
+
+def pack_palette(stream,palette):
+ palette.tofile(stream)
+
+
+def unpack_palette(stream,palette_format,entry_count):
+ if palette_format == gx.TL_IA8:
+ palette_type = PaletteIA8
+ elif palette_format == gx.TL_RGB565:
+ palette_type = PaletteRGB565
+ elif palette_format == gx.TL_RGB5A3:
+ palette_type = PaletteRGB5A3
+ else:
+ raise ValueError('invalid palette format')
+
+ palette = numpy.fromfile(stream,palette_type.entry_type,entry_count)
+ return palette.view(palette_type)
+
+
+def pack_images(stream,images):
+ for image in images:
+ image.tofile(stream)
+
+
+def unpack_images(stream,image_format,base_width,base_height,level_count):
+ if image_format == gx.TF_I4:
+ image_type = ImageI4
+ elif image_format == gx.TF_I8:
+ image_type = ImageI8
+ elif image_format == gx.TF_IA4:
+ image_type = ImageIA4
+ elif image_format == gx.TF_IA8:
+ image_type = ImageIA8
+ elif image_format == gx.TF_RGB565:
+ image_type = ImageRGB565
+ elif image_format == gx.TF_RGB5A3:
+ image_type = ImageRGB5A3
+ elif image_format == gx.TF_RGBA8:
+ image_type = ImageRGBA8
+ elif image_format == gx.TF_CI4:
+ image_type = ImageCI4
+ elif image_format == gx.TF_CI8:
+ image_type = ImageCI8
+ elif image_format == gx.TF_CI14:
+ image_type = ImageCI14
+ elif image_format == gx.TF_CMPR:
+ image_type = ImageCMPR
+ else:
+ raise ValueError('invalid image format')
+
+ images = [None]*level_count
+
+ for level in range(level_count):
+ width = max(base_width//(2**level),1)
+ height = max(base_height//(2**level),1)
+
+ col_count = (width + image_type.tile_width - 1)//image_type.tile_width
+ row_count = (height + image_type.tile_height - 1)//image_type.tile_height
+ image = numpy.fromfile(stream,image_type.tile_type,col_count*row_count)
+ image = image.reshape((row_count,col_count) + image.shape[1:])
+ image = image.view(image_type)
+ image.width = width
+ image.height = height
+
+ images[level] = image
+
+ return tuple(images)
+
+
+class GLTexture(gl.Texture):
+
+ def __init__(self,images,palette):
+ super().__init__()
+
+ glBindTexture(GL_TEXTURE_2D,self)
+ glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_BASE_LEVEL,0)
+ glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAX_LEVEL,len(images) - 1)
+
+ if images[0].image_format in {gx.TF_CI4,gx.TF_CI8,gx.TF_CI14}:
+ component_count = palette.gl_component_count
+ image_format = palette.gl_image_format
+ swizzle = palette.gl_swizzle
+ else:
+ component_count = images[0].gl_component_count
+ image_format = images[0].gl_image_format
+ swizzle = images[0].gl_swizzle
+
+ glTexParameteriv(GL_TEXTURE_2D,GL_TEXTURE_SWIZZLE_RGBA,swizzle)
+
+ for level,image in enumerate(images):
+ glTexImage2D(GL_TEXTURE_2D,level,component_count,image.width,image.height,0,component_count,image_format,image.gl_convert(palette))
+
+
+class Texture:
+
+ def __init__(self):
+ self.wrap_s = gx.CLAMP
+ self.wrap_t = gx.CLAMP
+ self.minification_filter = gx.NEAR
+ self.magnification_filter = gx.NEAR
+ self.minimum_lod = 0
+ self.maximum_lod = 0
+ self.lod_bias = 0
+ self.images = None
+ self.palette = None
+
+ def gl_init(self,texture_factory=GLTexture):
+ self.gl_wrap_s_need_update = True
+ self.gl_wrap_t_need_update = True
+ self.gl_minification_filter_need_update = True
+ self.gl_magnification_filter_need_update = True
+ self.gl_minimum_lod_need_update = True
+ self.gl_maximum_lod_need_update = True
+ self.gl_lod_bias_need_update = True
+
+ self.gl_sampler = gl.Sampler()
+ self.gl_texture = texture_factory(self.images,self.palette)
+
+ def gl_bind(self,texture_unit):
+ if self.gl_wrap_s_need_update:
+ glSamplerParameteri(self.gl_sampler,GL_TEXTURE_WRAP_S,self.wrap_s.gl_value)
+ self.gl_wrap_s_need_update = False
+ if self.gl_wrap_t_need_update:
+ glSamplerParameteri(self.gl_sampler,GL_TEXTURE_WRAP_T,self.wrap_t.gl_value)
+ self.gl_wrap_t_need_update = False
+ if self.gl_minification_filter_need_update:
+ glSamplerParameteri(self.gl_sampler,GL_TEXTURE_MIN_FILTER,self.minification_filter.gl_value)
+ self.gl_minification_filter_need_update = False
+ if self.gl_magnification_filter_need_update:
+ glSamplerParameteri(self.gl_sampler,GL_TEXTURE_MAG_FILTER,self.magnification_filter.gl_value)
+ self.gl_magnification_filter_need_update = False
+ if self.gl_minimum_lod_need_update:
+ glSamplerParameterf(self.gl_sampler,GL_TEXTURE_MIN_LOD,self.minimum_lod)
+ self.gl_minimum_lod_need_update = False
+ if self.gl_maximum_lod_need_update:
+ glSamplerParameterf(self.gl_sampler,GL_TEXTURE_MAX_LOD,self.maximum_lod)
+ self.gl_maximum_lod_need_update = False
+ if self.gl_lod_bias_need_update:
+ glSamplerParameterf(self.gl_sampler,GL_TEXTURE_LOD_BIAS,self.lod_bias)
+ self.gl_lod_bias_need_update = False
+
+ glBindSampler(texture_unit,self.gl_sampler)
+ glActiveTexture(GL_TEXTURE0 + texture_unit)
+ glBindTexture(GL_TEXTURE_2D,self.gl_texture)
+
diff --git a/j3d/__init__.py b/j3d/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/j3d/animation.py b/j3d/animation.py
new file mode 100644
index 0000000..b5e6bd8
--- /dev/null
+++ b/j3d/animation.py
@@ -0,0 +1,133 @@
+from btypes.big_endian import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+class IncompatibleAnimationError(Exception): pass
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ file_type = ByteString(4)
+ file_size = uint32
+ section_count = uint32
+ unknown0 = ByteString(4)
+ __padding__ = Padding(12)
+
+
+class KeyFrame:
+
+ def __init__(self,time,value,tangent_in,tangent_out=None):
+ self.time = time
+ self.value = value
+ self.tangent_in = tangent_in
+ self.tangent_out = tangent_out if tangent_out is not None else tangent_in
+
+
+class ConstantInterpolater:
+
+ def __init__(self,value):
+ self.value = value
+
+ def interpolate(self,time):
+ return self.value
+
+
+class CubicSplineInterpolater:
+
+ def __init__(self,keys):
+ self.keys = keys
+
+ def interpolate(self,time):
+ if self.keys[-1].time < time:
+ return self.keys[-1].value
+
+ i = 1
+ while self.keys[i].time < time: i += 1
+
+ t = (time - self.keys[i - 1].time)/(self.keys[i].time - self.keys[i - 1].time)
+ a = 2*(self.keys[i - 1].value - self.keys[i].value) + self.keys[i - 1].tangent_out + self.keys[i].tangent_in
+ b = -3*self.keys[i - 1].value + 3*self.keys[i].value - 2*self.keys[i - 1].tangent_out - self.keys[i].tangent_in
+ c = self.keys[i - 1].tangent_out
+ d = self.keys[i - 1].value
+ return ((a*t + b)*t + c)*t + d
+
+
+class Animation:
+
+ def __init__(self,duration,loop_mode):
+ self.duration = duration
+ self.loop_mode = loop_mode
+
+ def attach(self,model): #<-?
+ self.time = -1
+
+ @property
+ def is_finished(self):
+ return self.time == self.duration and self.loop_mode == 0
+
+ def advance_frame(self):
+ self.time += 1
+ if self.time == self.duration and self.loop_mode == 2:
+ self.time = 0
+ self.update_model()
+
+
+def select_interpolater(selection,array,scale=None):
+ if selection.count == 1:
+ interpolater = ConstantInterpolater(array[selection.first])
+ elif selection.unknown0 == 0:
+ interpolater = CubicSplineInterpolater([KeyFrame(*array[selection.first + 3*i:selection.first + 3*i + 3]) for i in range(selection.count)])
+ elif selection.unknown0 == 1:
+ interpolater = CubicSplineInterpolater([KeyFrame(*array[selection.first + 4*i:selection.first + 4*i + 4]) for i in range(selection.count)])
+ else:
+ raise ValueError('invalid selection unknkown0')
+
+ if scale is not None:
+ if isinstance(interpolater,ConstantInterpolater):
+ interpolater.value *= scale
+ else:
+ for key in interpolater.keys:
+ key.value *= scale
+ key.tangent_in *= scale
+ key.tangent_out *= scale
+
+ return interpolater
+
+
+import j3d.vaf1
+import j3d.ank1
+import j3d.pak1
+import j3d.trk1
+import j3d.tpt1
+import j3d.ttk1
+
+
+def unpack(stream):
+ header = Header.unpack(stream)
+ if header.magic != b'J3D1':
+ raise FormatError('invalid magic')
+ if header.section_count != 1:
+ raise FormatError('invalid section count')
+ if header.unknown0 not in {b'\xFF\xFF\xFF\xFF',b'SVR1',b'SVR3'}:
+ logger.warning('unknown0 different from default')
+
+ if header.file_type == b'bva1':
+ animation = j3d.vaf1.unpack(stream)
+ elif header.file_type == b'bck1':
+ animation = j3d.ank1.unpack(stream)
+ elif header.file_type == b'bpk1':
+ animation = j3d.pak1.unpack(stream)
+ elif header.file_type == b'brk1':
+ animation = j3d.trk1.unpack(stream)
+ elif header.file_type == b'btp1':
+ animation = j3d.tpt1.unpack(stream)
+ elif header.file_type == b'btk1':
+ animation = j3d.ttk1.unpack(stream)
+ else:
+ raise FormatError('invalid file type')
+
+ animation.unknown0 = header.unknown0
+
+ return animation
+
diff --git a/j3d/ank1.py b/j3d/ank1.py
new file mode 100644
index 0000000..bff35c5
--- /dev/null
+++ b/j3d/ank1.py
@@ -0,0 +1,99 @@
+from btypes.big_endian import *
+from j3d.animation import Animation,select_interpolater,IncompatibleAnimationError
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ loop_mode = uint8
+ angle_scale_exponent = uint8
+ duration = uint16
+ joint_animation_count = uint16
+ scale_count = uint16
+ rotation_count = uint16
+ translation_count = uint16
+ joint_animation_offset = uint32
+ scale_offset = uint32
+ rotation_offset = uint32
+ translation_offset = uint32
+
+
+class Selection(Struct):
+ count = uint16
+ first = uint16
+ unknown0 = uint16
+
+
+class ComponentAnimation(Struct):
+ scale_selection = Selection
+ rotation_selection = Selection
+ translation_selection = Selection
+
+
+class JointAnimation(Struct):
+ x = ComponentAnimation
+ y = ComponentAnimation
+ z = ComponentAnimation
+
+
+class SkeletalAnimation(Animation):
+
+ def __init__(self,duration,loop_mode,joint_animations):
+ super().__init__(duration,loop_mode)
+ self.joint_animations = joint_animations
+
+ def attach(self,model):
+ if len(self.joint_animations) != len(model.joints):
+ raise IncompatibleAnimationError()
+ self.time = -1
+ self.model = model
+
+ def update_model(self):
+ for joint_animation,joint in zip(self.joint_animations,self.model.gl_joints):
+ joint.scale_x = joint_animation.x.scale.interpolate(self.time)
+ joint.rotation_x = joint_animation.x.rotation.interpolate(self.time)
+ joint.translation_x = joint_animation.x.translation.interpolate(self.time)
+ joint.scale_y = joint_animation.y.scale.interpolate(self.time)
+ joint.rotation_y = joint_animation.y.rotation.interpolate(self.time)
+ joint.translation_y = joint_animation.y.translation.interpolate(self.time)
+ joint.scale_z = joint_animation.z.scale.interpolate(self.time)
+ joint.rotation_z = joint_animation.z.rotation.interpolate(self.time)
+ joint.translation_z = joint_animation.z.translation.interpolate(self.time)
+
+ self.model.gl_update_matrix_table()
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+ if header.magic != b'ANK1':
+ raise FormatError('invalid magic')
+
+ stream.seek(base + header.joint_animation_offset)
+ joint_animations = [JointAnimation.unpack(stream) for _ in range(header.joint_animation_count)]
+
+ stream.seek(base + header.scale_offset)
+ scales = [float32.unpack(stream) for _ in range(header.scale_count)]
+
+ stream.seek(base + header.rotation_offset)
+ rotations = [sint16.unpack(stream) for _ in range(header.rotation_count)]
+
+ stream.seek(base + header.translation_offset)
+ translations = [float32.unpack(stream) for _ in range(header.translation_count)]
+
+ angle_scale = 180/32767*2**header.angle_scale_exponent
+
+ for joint_animation in joint_animations:
+ joint_animation.x.scale = select_interpolater(joint_animation.x.scale_selection,scales)
+ joint_animation.x.rotation = select_interpolater(joint_animation.x.rotation_selection,rotations,angle_scale)
+ joint_animation.x.translation = select_interpolater(joint_animation.x.translation_selection,translations)
+ joint_animation.y.scale = select_interpolater(joint_animation.y.scale_selection,scales)
+ joint_animation.y.rotation = select_interpolater(joint_animation.y.rotation_selection,rotations,angle_scale)
+ joint_animation.y.translation = select_interpolater(joint_animation.y.translation_selection,translations)
+ joint_animation.z.scale = select_interpolater(joint_animation.z.scale_selection,scales)
+ joint_animation.z.rotation = select_interpolater(joint_animation.z.rotation_selection,rotations,angle_scale)
+ joint_animation.z.translation = select_interpolater(joint_animation.z.translation_selection,translations)
+
+ stream.seek(base + header.section_size)
+ return SkeletalAnimation(header.duration,header.loop_mode,joint_animations)
+
diff --git a/j3d/drw1.py b/j3d/drw1.py
new file mode 100644
index 0000000..32c524d
--- /dev/null
+++ b/j3d/drw1.py
@@ -0,0 +1,74 @@
+from enum import IntEnum
+from btypes.big_endian import *
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ matrix_descriptor_count = uint16
+ __padding__ = Padding(2)
+ matrix_type_offset = uint32
+ index_offset = uint32
+
+ def __init__(self):
+ self.magic = b'DRW1'
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'DRW1':
+ raise FormatError('invalid magic')
+ return header
+
+
+class MatrixType(IntEnum):
+ JOINT = 0
+ INFLUENCE_GROUP = 1
+
+
+class MatrixDescriptor:
+
+ def __init__(self,matrix_type,index):
+ self.matrix_type = matrix_type
+ self.index = index
+
+
+def pack(stream,matrix_descriptors):
+ base = stream.tell()
+ header = Header()
+ header.matrix_descriptor_count = len(matrix_descriptors)
+ stream.write(b'\x00'*Header.sizeof())
+
+ header.matrix_type_offset = stream.tell() - base
+ for matrix_descriptor in matrix_descriptors:
+ uint8.pack(stream,matrix_descriptor.matrix_type)
+
+ align(stream,2)
+ header.index_offset = stream.tell() - base
+ for matrix_descriptor in matrix_descriptors:
+ uint16.pack(stream,matrix_descriptor.index)
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+ stream.seek(base)
+ Header.pack(stream,header)
+ stream.seek(base + header.section_size)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+
+ matrix_descriptors = [MatrixDescriptor(None,None) for _ in range(header.matrix_descriptor_count)]
+
+ stream.seek(base + header.matrix_type_offset)
+ for matrix_descriptor in matrix_descriptors:
+ matrix_descriptor.matrix_type = MatrixType(uint8.unpack(stream))
+
+ stream.seek(base + header.index_offset)
+ for matrix_descriptor in matrix_descriptors:
+ matrix_descriptor.index = uint16.unpack(stream)
+
+ stream.seek(base + header.section_size)
+ return matrix_descriptors
+
diff --git a/j3d/evp1.py b/j3d/evp1.py
new file mode 100644
index 0000000..51fab34
--- /dev/null
+++ b/j3d/evp1.py
@@ -0,0 +1,100 @@
+import numpy
+from btypes.big_endian import *
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ influence_group_count = uint16
+ __padding__ = Padding(2)
+ influence_count_offset = uint32
+ index_offset = uint32
+ weight_offset = uint32
+ inverse_bind_matrix_offset = uint32
+
+ def __init__(self):
+ self.magic = b'EVP1'
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'EVP1':
+ raise FormatError('invalid magic')
+ return header
+
+
+class Influence:
+
+ def __init__(self,index,weight):
+ self.index = index
+ self.weight = weight
+
+
+def pack(stream,influence_groups,inverse_bind_matrices):
+ base = stream.tell()
+ header = Header()
+ header.influence_group_count = len(influence_groups)
+ header.influence_count_offset = 0
+ header.index_offset = 0
+ header.weight_offset = 0
+ header.inverse_bind_matrix_offset = 0
+ stream.write(b'\x00'*Header.sizeof())
+
+ if influence_groups:
+ header.influence_count_offset = stream.tell() - base
+ for influence_group in influence_groups:
+ uint8.pack(stream,len(influence_group))
+
+ header.index_offset = stream.tell() - base
+ for influence_group in influence_groups:
+ for influence in influence_group:
+ uint16.pack(stream,influence.index)
+
+ align(stream,4)
+ header.weight_offset = stream.tell() - base
+ for influence_group in influence_groups:
+ for influence in influence_group:
+ float32.pack(stream,influence.weight)
+
+ if inverse_bind_matrices is not None:
+ header.inverse_bind_matrix_offset = stream.tell() - base
+ inverse_bind_matrices.tofile(stream)
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+ stream.seek(base)
+ Header.pack(stream,header)
+ stream.seek(base + header.section_size)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+
+ influence_groups = [None]*header.influence_group_count
+ inverse_bind_matrices = None
+
+ stream.seek(base + header.influence_count_offset)
+ for i in range(header.influence_group_count):
+ influence_count = uint8.unpack(stream)
+ influence_groups[i] = [Influence(None,None) for _ in range(influence_count)]
+
+ stream.seek(base + header.index_offset)
+ for influence_group in influence_groups:
+ for influence in influence_group:
+ influence.index = uint16.unpack(stream)
+
+ stream.seek(base + header.weight_offset)
+ for influence_group in influence_groups:
+ for influence in influence_group:
+ influence.weight = float32.unpack(stream)
+
+ if header.inverse_bind_matrix_offset != 0:
+ stream.seek(base + header.inverse_bind_matrix_offset)
+ element_type = numpy.dtype((numpy.float32,(3,4))).newbyteorder('>')
+ element_count = (header.section_size - header.inverse_bind_matrix_offset)//element_type.itemsize
+ inverse_bind_matrices = numpy.fromfile(stream,element_type,element_count)
+
+ stream.seek(base + header.section_size)
+ return influence_groups,inverse_bind_matrices
+
diff --git a/j3d/fragment_shader.py b/j3d/fragment_shader.py
new file mode 100644
index 0000000..ead6d86
--- /dev/null
+++ b/j3d/fragment_shader.py
@@ -0,0 +1,732 @@
+from io import StringIO
+import gx
+from OpenGL.GL.ARB.shader_image_load_store import *
+
+
+class Scalar:
+
+ def __init__(self,value,component_count=4):
+ self.value = value
+ self.component_count = component_count
+
+ def __getitem__(self,key):
+ return getattr(self,key)
+
+ def __getattr__(self,attribute):
+ return Scalar(self.value,len(attribute))
+
+ def __eq__(self,other):
+ if not isinstance(other,Scalar):
+ return self.value == other
+ return self.value == other.value and self.component_count == other.component_count
+
+ def __str__(self):
+ if self.component_count == 1:
+ return str(self.value)
+ return 'vec{}({})'.format(self.component_count,self.value)
+
+ def swap(self,table): #<-?
+ return self
+
+
+class Vector: #<-?
+
+ component_map = {'r':0,'g':1,'b':2,'a':3} #<-?
+
+ def __init__(self,name,components='rgba'):
+ self.name = name
+ self.components = components
+
+ def __getitem__(self,key):
+ return getattr(self,key)
+
+ def __getattr__(self,attribute):
+ return Vector(self.name,''.join(self.components[self.component_map[component]] for component in attribute))
+
+ def __eq__(self,other):
+ if not isinstance(other,Vector):
+ return False
+ return self.name == other.name and self.components == other.components
+
+ def __str__(self):
+ if self.components == 'rgba':
+ return self.name
+ return self.name + '.' + self.components
+
+ def swap(self,table): #<-?
+ r = self.components[table.r]
+ g = self.components[table.g]
+ b = self.components[table.b]
+ a = self.components[table.a]
+ return Vector(self.name,r + g + b + a)
+
+#------------------------------------------------------------------------------
+
+def convert_indirect_texcoord_scale(scale):
+ if scale == gx.ITS_1:
+ return 1
+ if scale == gx.ITS_2:
+ return 1/2
+ if scale == gx.ITS_4:
+ return 1/4
+ if scale == gx.ITS_8:
+ return 1/8
+ if scale == gx.ITS_16:
+ return 1/16
+ if scale == gx.ITS_32:
+ return 1/32
+ if scale == gx.ITS_64:
+ return 1/64
+ if scale == gx.ITS_128:
+ return 1/128
+ if scale == gx.ITS_256:
+ return 1/256
+
+ raise ValueError('invalid indirect texture coordinate scale')
+
+
+def write_indirect_stage(stream,index,stage):
+ stream.write('vec3 indtex{} = texture(texmap{},'.format(index,stage.texture.index))
+ if stage.scale_s != gx.ITS_1 or stage.scale_t != gx.ITS_1:
+ scale_s = convert_indirect_texcoord_scale(stage.scale_s)
+ scale_t = convert_indirect_texcoord_scale(stage.scale_t)
+ stream.write('vec2({},{})*'.format(scale_s,scale_t))
+ stream.write('uv{}).abg;\n'.format(stage.texcoord.index))
+
+#------------------------------------------------------------------------------
+
+def convert_ras(color): #<-?
+ if color in (gx.COLOR0,gx.ALPHA0,gx.COLOR0A0):
+ return Vector('channel0')
+ if color in (gx.COLOR1,gx.ALPHA1,gx.COLOR1A1):
+ return Vector('channel1')
+ if color in (gx.ALPHA_BUMP,gx.ALPHA_BUMPN):
+ return Scalar('alphabump')
+ if color == gx.COLOR_ZERO:
+ return Scalar(0.0)
+ if color == gx.COLOR_NULL:
+ return Scalar(0.0) #TODO
+
+ raise ValueError('invalid TEV rasterized color')
+
+
+def convert_kcolorsel(kcolorsel):
+ if kcolorsel == gx.TEV_KCSEL_1:
+ return Scalar(1.0,3)
+ if kcolorsel == gx.TEV_KCSEL_7_8:
+ return Scalar(0.875,3)
+ if kcolorsel == gx.TEV_KCSEL_3_4:
+ return Scalar(0.75,3)
+ if kcolorsel == gx.TEV_KCSEL_5_8:
+ return Scalar(0.625,3)
+ if kcolorsel == gx.TEV_KCSEL_1_2:
+ return Scalar(0.5,3)
+ if kcolorsel == gx.TEV_KCSEL_3_8:
+ return Scalar(0.375,3)
+ if kcolorsel == gx.TEV_KCSEL_1_4:
+ return Scalar(0.25,3)
+ if kcolorsel == gx.TEV_KCSEL_1_8:
+ return Scalar(0.125,3)
+ if kcolorsel == gx.TEV_KCSEL_K0:
+ return Vector('kcolor0').rgb
+ if kcolorsel == gx.TEV_KCSEL_K1:
+ return Vector('kcolor1').rgb
+ if kcolorsel == gx.TEV_KCSEL_K2:
+ return Vector('kcolor2').rgb
+ if kcolorsel == gx.TEV_KCSEL_K3:
+ return Vector('kcolor3').rgb
+ if kcolorsel == gx.TEV_KCSEL_K0_R:
+ return Vector('kcolor0').rrr
+ if kcolorsel == gx.TEV_KCSEL_K1_R:
+ return Vector('kcolor1').rrr
+ if kcolorsel == gx.TEV_KCSEL_K2_R:
+ return Vector('kcolor2').rrr
+ if kcolorsel == gx.TEV_KCSEL_K3_R:
+ return Vector('kcolor3').rrr
+ if kcolorsel == gx.TEV_KCSEL_K0_G:
+ return Vector('kcolor0').ggg
+ if kcolorsel == gx.TEV_KCSEL_K1_G:
+ return Vector('kcolor1').ggg
+ if kcolorsel == gx.TEV_KCSEL_K2_G:
+ return Vector('kcolor2').ggg
+ if kcolorsel == gx.TEV_KCSEL_K3_G:
+ return Vector('kcolor3').ggg
+ if kcolorsel == gx.TEV_KCSEL_K0_B:
+ return Vector('kcolor0').bbb
+ if kcolorsel == gx.TEV_KCSEL_K1_B:
+ return Vector('kcolor1').bbb
+ if kcolorsel == gx.TEV_KCSEL_K2_B:
+ return Vector('kcolor2').bbb
+ if kcolorsel == gx.TEV_KCSEL_K3_B:
+ return Vector('kcolor3').bbb
+ if kcolorsel == gx.TEV_KCSEL_K0_A:
+ return Vector('kcolor0').aaa
+ if kcolorsel == gx.TEV_KCSEL_K1_A:
+ return Vector('kcolor1').aaa
+ if kcolorsel == gx.TEV_KCSEL_K2_A:
+ return Vector('kcolor2').aaa
+ if kcolorsel == gx.TEV_KCSEL_K3_A:
+ return Vector('kcolor3').aaa
+
+ raise ValueError('invalid TEV constant color selection')
+
+
+def convert_kalphasel(kalphasel):
+ if kalphasel == gx.TEV_KASEL_1:
+ return Scalar(1.0,1)
+ if kalphasel == gx.TEV_KASEL_7_8:
+ return Scalar(0.875,1)
+ if kalphasel == gx.TEV_KASEL_3_4:
+ return Scalar(0.75,1)
+ if kalphasel == gx.TEV_KASEL_5_8:
+ return Scalar(0.625,1)
+ if kalphasel == gx.TEV_KASEL_1_2:
+ return Scalar(0.5,1)
+ if kalphasel == gx.TEV_KASEL_3_8:
+ return Scalar(0.375,1)
+ if kalphasel == gx.TEV_KASEL_1_4:
+ return Scalar(0.25,1)
+ if kalphasel == gx.TEV_KASEL_1_8:
+ return Scalar(0.125,1)
+ if kalphasel == gx.TEV_KASEL_K0_R:
+ return Vector('kcolor0').r
+ if kalphasel == gx.TEV_KASEL_K1_R:
+ return Vector('kcolor1').r
+ if kalphasel == gx.TEV_KASEL_K2_R:
+ return Vector('kcolor2').r
+ if kalphasel == gx.TEV_KASEL_K3_R:
+ return Vector('kcolor3').r
+ if kalphasel == gx.TEV_KASEL_K0_G:
+ return Vector('kcolor0').g
+ if kalphasel == gx.TEV_KASEL_K1_G:
+ return Vector('kcolor1').g
+ if kalphasel == gx.TEV_KASEL_K2_G:
+ return Vector('kcolor2').g
+ if kalphasel == gx.TEV_KASEL_K3_G:
+ return Vector('kcolor3').g
+ if kalphasel == gx.TEV_KASEL_K0_B:
+ return Vector('kcolor0').b
+ if kalphasel == gx.TEV_KASEL_K1_B:
+ return Vector('kcolor1').b
+ if kalphasel == gx.TEV_KASEL_K2_B:
+ return Vector('kcolor2').b
+ if kalphasel == gx.TEV_KASEL_K3_B:
+ return Vector('kcolor3').b
+ if kalphasel == gx.TEV_KASEL_K0_A:
+ return Vector('kcolor0').a
+ if kalphasel == gx.TEV_KASEL_K1_A:
+ return Vector('kcolor1').a
+ if kalphasel == gx.TEV_KASEL_K2_A:
+ return Vector('kcolor2').a
+ if kalphasel == gx.TEV_KASEL_K3_A:
+ return Vector('kcolor3').a
+
+ raise ValueError('invalid TEV constant alpha selection')
+
+
+def convert_color_input(color_input,color,texture,konst):
+ if color_input == gx.CC_CPREV:
+ return Vector('tevprev')
+ if color_input == gx.CC_APREV:
+ return Vector('tevprev').aaaa
+ if color_input == gx.CC_C0:
+ return Vector('tevreg0')
+ if color_input == gx.CC_A0:
+ return Vector('tevreg0').aaaa
+ if color_input == gx.CC_C1:
+ return Vector('tevreg1')
+ if color_input == gx.CC_A1:
+ return Vector('tevreg1').aaaa
+ if color_input == gx.CC_C2:
+ return Vector('tevreg2')
+ if color_input == gx.CC_A2:
+ return Vector('tevreg2').aaaa
+ if color_input == gx.CC_TEXC:
+ return texture
+ if color_input == gx.CC_TEXA:
+ return texture.aaaa
+ if color_input == gx.CC_RASC:
+ return color
+ if color_input == gx.CC_RASA:
+ return texture.aaaa
+ if color_input == gx.CC_ONE:
+ return Scalar(1.0)
+ if color_input == gx.CC_HALF:
+ return Scalar(0.5)
+ if color_input == gx.CC_KONST:
+ return konst
+ if color_input == gx.CC_ZERO:
+ return Scalar(0.0)
+
+ raise ValueError('inavlid TEV color combiner input')
+
+
+def convert_color_input_overflow(color_input,color,texture,konst):
+ if color_input == gx.CC_CPREV:
+ return Vector('(fract(tevprev*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_APREV:
+ return Scalar('(fract(tevprev.a*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_C0:
+ return Vector('(fract(tevreg0*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_A0:
+ return Scalar('(fract(tevreg0.a*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_C1:
+ return Vector('(fract(tevreg1*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_A1:
+ return Scalar('(fract(tevreg1.a*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_C2:
+ return Vector('(fract(tevreg2*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_A2:
+ return Scalar('(fract(tevreg2.a*(255.0/256.0))*(256.0/255.0))')
+ if color_input == gx.CC_TEXC:
+ return texture
+ if color_input == gx.CC_TEXA:
+ return texture.aaaa
+ if color_input == gx.CC_RASC:
+ return color
+ if color_input == gx.CC_RASA:
+ return texture.aaaa
+ if color_input == gx.CC_ONE:
+ return Scalar(1.0)
+ if color_input == gx.CC_HALF:
+ return Scalar(0.5)
+ if color_input == gx.CC_KONST:
+ return konst
+ if color_input == gx.CC_ZERO:
+ return Scalar(0.0)
+
+ raise ValueError('inavlid TEV color combiner input')
+
+
+def convert_alpha_input(alpha_input,color,texture,konst):
+ if alpha_input == gx.CA_APREV:
+ return Vector('tevprev')
+ if alpha_input == gx.CA_A0:
+ return Vector('tevreg0')
+ if alpha_input == gx.CA_A1:
+ return Vector('tevreg1')
+ if alpha_input == gx.CA_A2:
+ return Vector('tevreg2')
+ if alpha_input == gx.CA_TEXA:
+ return texture
+ if alpha_input == gx.CA_RASA:
+ return color
+ if alpha_input == gx.CA_KONST:
+ return konst
+ if alpha_input == gx.CA_ZERO:
+ return Scalar(0.0)
+
+ raise ValueError('invalide TEV alpha combiner input')
+
+
+def convert_alpha_input_overflow(alpha_input,color,texture,konst):
+ if alpha_input == gx.CA_APREV:
+ return Vector('(fract(tevprev*(255.0/256.0))*(256.0/255.0))')
+ if alpha_input == gx.CA_A0:
+ return Vector('(fract(tevreg0*(255.0/256.0))*(256.0/255.0))')
+ if alpha_input == gx.CA_A1:
+ return Vector('(fract(tevreg1*(255.0/256.0))*(256.0/255.0))')
+ if alpha_input == gx.CA_A2:
+ return Vector('(fract(tevreg2*(255.0/256.0))*(256.0/255.0))')
+ if alpha_input == gx.CA_TEXA:
+ return texture
+ if alpha_input == gx.CA_RASA:
+ return color
+ if alpha_input == gx.CA_KONST:
+ return konst
+ if alpha_input == gx.CA_ZERO:
+ return Scalar(0.0)
+
+ raise ValueError('invalide TEV alpha combiner input')
+
+
+def convert_output(output):
+ if output == gx.TEVPREV:
+ return Vector('tevprev')
+ if output == gx.TEVREG0:
+ return Vector('tevreg0')
+ if output == gx.TEVREG1:
+ return Vector('tevreg1')
+ if output == gx.TEVREG2:
+ return Vector('tevreg2')
+
+ raise ValueError('invalid TEV combiner output')
+
+
+def convert_operator(operator): #<-?
+ if operator == gx.TEV_ADD:
+ return '+'
+ if operator == gx.TEV_SUB:
+ return '-'
+
+ raise ValueError('invalid TEV operator')
+
+
+def convert_tevscale(scale):
+ if scale == gx.CS_SCALE_1:
+ return '1.0'
+ if scale == gx.CS_SCALE_2:
+ return '2.0'
+ if scale == gx.CS_SCALE_4:
+ return '4.0'
+ if scale == gx.CS_DIVIDE_2:
+ return '0.5'
+
+ raise ValueError('invalid TEV scale')
+
+
+def pack_combiner(stream,mode,a,b,c,d,swizzle):
+ stream.write('{} = '.format(convert_output(mode.output)[swizzle]))
+
+ if mode.clamp:
+ stream.write('clamp(')
+
+ if mode.function in (gx.TEV_ADD,gx.TEV_SUB):
+ if mode.scale != gx.CS_SCALE_1:
+ stream.write('{}*('.format(convert_tevscale(mode.scale)))
+
+ if not (d[swizzle] == 0 and mode.function == gx.TEV_ADD): #<-?
+ stream.write('{} {} '.format(d[swizzle],convert_operator(mode.function)))
+
+ if a[swizzle] == b[swizzle] or c[swizzle] == 0:
+ stream.write(str(a[swizzle]))
+ elif c[swizzle] == 1:
+ stream.write(str(b[swizzle]))
+ elif a[swizzle] == 0:
+ stream.write('{}*{}'.format(c[swizzle],b[swizzle]))
+ elif b[swizzle] == 0:
+ stream.write('(1.0 - {})*{}'.format(c[swizzle],a[swizzle]))
+ else:
+ stream.write('mix({},{},{})'.format(a[swizzle],b[swizzle],c[swizzle]))
+
+ if mode.bias == gx.TB_ZERO: #<-?
+ pass
+ elif mode.bias == gx.TB_ADDHALF:
+ stream.write(' + 0.5')
+ elif mode.bias == gx.TB_SUBHALF:
+ stream.write(' - 0.5')
+ else:
+ raise ValueError('invalid TEV bias')
+
+ if mode.scale != gx.CS_SCALE_1:
+ stream.write(')')
+
+ elif mode.function == gx.TEV_COMP_R8_GT:
+ if swizzle == 'rgb':
+ stream.write('{} + (({} >= {} + (0.25/255.0)) ? {} : vec3(0.0))'.format(d[swizzle],a.r,b.r,c[swizzle]))
+ else:
+ stream.write('{} + (({} >= {} + (0.25/255.0)) ? {} : 0.0)'.format(d[swizzle],a.r,b.r,c[swizzle]))
+ elif mode.function == gx.TEV_COMP_R8_EQ:
+ if swizzle == 'rgb':
+ stream.write('{} + ((abs({} - {}) < (0.5/255.0) ? {} : vec3(0.0))'.format(d[swizzle],a.r,b.r,c[swizzle]))
+ else:
+ stream.write('{} + ((abs({} - {}) < (0.5/255.0) ? {} : 0.0)'.format(d[swizzle],a.r,b.r,c[swizzle]))
+ elif mode.function == gx.TEV_COMP_GR16_GT:
+ if swizzle == 'rgb':
+ stream.write('{} + ((dot({},vec2(1.0,255.0)) >= (dot({},vec2(1.0,255.0)) + (0.25/255.0))) ? {} : vec3(0.0))'.format(d[swizzle],a.rg,b.rg,c[swizzle]))
+ else:
+ stream.write('{} + ((dot({},vec2(1.0,255.0)) >= (dot({},vec2(1.0,255.0)) + (0.25/255.0))) ? {} : 0.0)'.format(d[swizzle],a.rg,b.rg,c[swizzle]))
+ elif mode.function == gx.TEV_COMP_GR16_EQ:
+ if swizzle == 'rgb':
+ stream.write('{} + (abs(dot({},vec2(1.0,255.0)) - dot({},vec2(1.0,255.0))) < (0.5/255.0) ? {} : vec3(0.0))'.format(d[swizzle],a.rg,b.rg,c[swizzle]))
+ else:
+ stream.write('{} + (abs(dot({},vec2(1.0,255.0)) - dot({},vec2(1.0,255.0))) < (0.5/255.0) ? {} : 0.0)'.format(d[swizzle],a.rg,b.rg,c[swizzle]))
+ elif mode.function == gx.TEV_COMP_BGR24_GT:
+ if swizzle == 'rgb':
+ stream.write('{} + ((dot({},vec3(1.0,255.0,255.0*255.0)) >= (dot({},vec3(1.0,255.0,255.0*255.0)) + (0.25/255.0))) ? {} : vec3(0.0))'.format(d[swizzle],a.rgb,b.rgb,c[swizzle]))
+ else:
+ stream.write('{} + ((dot({},vec3(1.0,255.0,255.0*255.0)) >= (dot({},vec3(1.0,255.0,255.0*255.0)) + (0.25/255.0))) ? {} : 0.0)'.format(d[swizzle],a.rgb,b.rgb,c[swizzle]))
+ elif mode.function == gx.TEV_COMP_BGR24_EQ:
+ if swizzle == 'rgb':
+ stream.write('{} + (abs(dot({},vec3(1.0,255.0,255.0*255.0)) - dot({},vec3(1.0,255.0,255.0*255.0))) < (0.5/255.0) ? {} : vec3(0.0))'.format(d[swizzle],a.rgb,b.rgb,c[swizzle]))
+ else:
+ stream.write('{} + (abs(dot({},vec3(1.0,255.0,255.0*255.0)) - dot({},vec3(1.0,255.0,255.0*255.0))) < (0.5/255.0) ? {} : 0.0)'.format(d[swizzle],a.rgb,b.rgb,c[swizzle]))
+ elif mode.function == gx.TEV_COMP_RGB8_GT:
+ if swizzle == 'rgb':
+ stream.write('{} + (max(sign({} - {} - (0.25/255.0)),0.0)*{})'.format(d[swizzle],a.rgb,b.rgb,c[swizzle]))
+ else:
+ stream.write('{} + (({} >= ({} + (0.25/255.0))) ? {} : 0.0)'.format(d[swizzle],a.a,b.a,c[swizzle]))
+ elif mode.function == gx.TEV_COMP_RGB8_EQ:
+ if swizzle == 'rgb':
+ stream.write('{} + ((1.0 - max(sign(abs({} - {}) - (0.5/255.0)),0.0))*{})'.format(d[swizzle],a.rgb,b.rgb,c[swizzle]))
+ else:
+ stream.write('{} + (abs({} - {}) < (0.5/255.0) ? {} : 0.0)'.format(d[swizzle],a.a,b.a,c[swizzle]))
+ else:
+ raise ValueError('invalide TEV combiner function')
+
+ if mode.clamp:
+ stream.write(',0.0,1.0)')
+
+ stream.write(';\n')
+
+
+def convert_bump_alpha(selection): #<-?
+ if selection == gx.ITBA_S:
+ return 's'
+ if selection == gx.ITBA_T:
+ return 't'
+ if selection == gx.ITBA_U:
+ return 'p'
+
+ raise ValueError('invalid bump alpha')
+
+
+def convert_bias_selection(selection):
+ if selection == gx.ITB_S:
+ return 's'
+ if selection == gx.ITB_T:
+ return 't'
+ if selection == gx.ITB_U:
+ return 'p'
+ if selection == gx.ITB_ST:
+ return 'st'
+ if selection == gx.ITB_SU:
+ return 'sp'
+ if selection == gx.ITB_TU:
+ return 'tp'
+ if selection == gx.ITB_STU:
+ return 'stp'
+
+ raise ValueError('invalid indirect texture bias selection')
+
+
+def convert_wrap(wrap):
+ if wrap == gx.ITW_256:
+ return 256.0
+ if wrap == gx.ITW_128:
+ return 128.0
+ if wrap == gx.ITW_64:
+ return 64.0
+ if wrap == gx.ITW_32:
+ return 32.0
+ if wrap == gx.ITW_16:
+ return 16.0
+ if wrap == gx.ITW_0:
+ return 0.0
+
+ raise ValueError('invalid indirect texture wrap')
+
+
+def write_tev_stage(stream,stage,material):
+ if stage.indirect_format == gx.ITF_8:
+ indirect_scale = 2**8
+ alphabump_scale = 2**5
+ bias = -128
+ elif stage.indirect_format == gx.ITF_5:
+ indirect_scale = 2**5
+ alphabump_scale = 2**3
+ bias = 1
+ elif stage.indirect_format == gx.ITF_4:
+ indirect_scale = 2**4
+ alphabump_scale = 2**4
+ bias = 1
+ elif stage.indirect_format == gx.ITF_3:
+ indirect_scale = 2**3
+ alphabump_scale = 2**5
+ bias = 1
+ else:
+ raise ValueError('invalid indirect texture format')
+
+ if stage.bump_alpha != gx.ITBA_OFF:
+ stream.write('alphabump = ')
+ if stage.color == gx.ALPHA_BUMPN:
+ stream.write('(255.0/248.0)*')
+ stream.write('floor({0}*indtex{1}.{2})/{0}'.format(alphabump_scale,stage.indirect_stage.index,convert_bump_alpha(stage.bump_alpha))) #<-?
+ stream.write(';\n')
+
+ if stage.indirect_matrix == gx.ITM_OFF:
+ stream.write('indtevtrans = vec2(0.0);\n')
+ else:
+ if stage.indirect_format == gx.ITF_8:
+ stream.write('indtevcrd = 255.0*indtex{};\n'.format(stage.indirect_stage.index))
+ else:
+ stream.write('indtevcrd = mod(255.0*indtex{},{});\n'.format(stage.indirect_stage.index,indirect_scale))
+
+ if stage.indirect_bias_components != gx.ITB_NONE:
+ stream.write('indtevcrd.{} += {};\n'.format(convert_bias_selection(stage.indirect_bias_components),bias))
+
+ if stage.indirect_matrix in gx.ITM:
+ stream.write('indtevtrans = indmatrix{}*indtevcrd;\n'.format(stage.indirect_matrix.index))
+ elif stage.indirect_matrix in gx.ITM_S:
+ scale = 2**material.indirect_matrices[gx.ITM_S.index(stage.indirect_matrix)].scale_exponent
+ stream.write('indtevtrans = {}*indtevcrd.s*uv{};\n'.format(scale/256,stage.texcoord.index))
+ elif stage.indirect_matrix in gx.ITM_T:
+ scale = 2**material.indirect_matrices[gx.ITM_T.index(stage.indirect_matrix)].scale_exponent
+ stream.write('indtevtrans = {}*indtevcrd.t*uv{};\n'.format(scale/256,stage.texcoord.index))
+ else:
+ raise ValueError('invalid indirect texture matrix')
+
+ #TODO: scaling of texcoords to texture size needs some work
+
+ if stage.texture != gx.TEXMAP_NULL:
+ stream.write('indtevtrans /= textureSize(texmap{},0);\n'.format(stage.texture.index))
+
+ if stage.texcoord == gx.TEXCOORD_NULL or stage.wrap_s == gx.ITW_0:
+ stream.write('wrappedcoord.s = 0.0;\n')
+ elif stage.wrap_s == gx.ITW_OFF:
+ stream.write('wrappedcoord.s = uv{}.s;\n'.format(stage.texcoord.index))
+ else:
+ stream.write('wrappedcoord.s = mod(uv{}.s,{});\n'.format(stage.texcoord.index,convert_wrap(stage.wrap_s)))
+
+ if stage.texcoord == gx.TEXCOORD_NULL or stage.wrap_t == gx.ITW_0:
+ stream.write('wrappedcoord.t = 0.0;\n')
+ elif stage.wrap_t == gx.ITW_OFF:
+ stream.write('wrappedcoord.t = uv{}.t;\n'.format(stage.texcoord.index))
+ else:
+ stream.write('wrappedcoord.t = mod(uv{}.t,{});\n'.format(stage.texcoord.index,convert_wrap(stage.wrap_s)))
+
+ if stage.add_previous_texcoord:
+ stream.write('tevcoord += wrappedcoord + indtevtrans;\n')
+ else:
+ stream.write('tevcoord = wrappedcoord + indtevtrans;\n')
+
+ if stage.texture != gx.TEXMAP_NULL:
+ if stage.use_original_lod:
+ lod = 'textureQueryLod(texmap{},uv{})'.format(stage.texture.index,stage.texcoord.index)
+ stream.write('textemp = textureLod(texmap{},tevcoord,{});\n'.format(stage.texture.index,lod))
+ else:
+ stream.write('textemp = texture(texmap{},tevcoord);\n'.format(stage.texture.index))
+
+ texture = Vector('textemp').swap(material.swap_tables[stage.texture_swap_table.index])
+ color = convert_ras(stage.color).swap(material.swap_tables[stage.color_swap_table.index])
+ konst = convert_kcolorsel(stage.constant_color)
+ konst.a = convert_kalphasel(stage.constant_alpha)
+
+ ca = convert_color_input_overflow(stage.color_mode.a,color,texture,konst)
+ cb = convert_color_input_overflow(stage.color_mode.b,color,texture,konst)
+ cc = convert_color_input_overflow(stage.color_mode.c,color,texture,konst)
+ cd = convert_color_input(stage.color_mode.d,color,texture,konst)
+
+ aa = convert_alpha_input_overflow(stage.alpha_mode.a,color,texture,konst)
+ ab = convert_alpha_input_overflow(stage.alpha_mode.b,color,texture,konst)
+ ac = convert_alpha_input_overflow(stage.alpha_mode.c,color,texture,konst)
+ ad = convert_alpha_input(stage.alpha_mode.d,color,texture,konst)
+
+ pack_combiner(stream,stage.color_mode,ca,cb,cc,cd,'rgb')
+ pack_combiner(stream,stage.alpha_mode,aa,ab,ac,ad,'a')
+
+#------------------------------------------------------------------------------
+
+def convert_alpha_comparison(function,reference):
+ if function == gx.CompareFunction.NEVER:
+ return 'false'
+ if function == gx.CompareFunction.LESS:
+ return 'tevprev.a <= {} - 0.25/255.0'.format(reference/255)
+ if function == gx.CompareFunction.EQUAL:
+ return 'abs(tevprev.a - {}) < 0.5/255.0'.format(reference/255)
+ if function == gx.CompareFunction.LEQUAL:
+ return 'tevprev.a < {} + 0.25/255.0'.format(reference/255)
+ if function == gx.CompareFunction.GREATER:
+ return 'tevprev.a >= {} + 0.25/255.0'.format(reference/255)
+ if function == gx.CompareFunction.NEQUAL:
+ return 'abs(tevprev.a - {}) >= 0.5/255.0'.format(reference/255)
+ if function == gx.CompareFunction.GEQUAL:
+ return 'tevprev.a > {} - 0.25/255.0'.format(reference/255)
+ if function == gx.CompareFunction.ALWAYS:
+ return 'true'
+
+ raise ValueError('invalid alpha compare function') #<-?
+
+
+def convert_alpha_test_operation(operation):
+ if operation == gx.AOP_AND:
+ return '&&'
+ if operation == gx.AOP_OR:
+ return '||'
+ if operation == gx.AOP_XOR:
+ return '!='
+ if operation == gx.AOP_XNOR:
+ return '=='
+
+ raise ValueError('invalid alpha test operation')
+
+
+def write_alpha_test(stream,test):
+ if test.operation == gx.AOP_AND:
+ never_pass = test.function0 == gx.CompareFunction.NEVER or test.function1 == gx.CompareFunction.NEVER
+ always_pass = test.function0 == test.function1 == gx.CompareFunction.ALWAYS
+ elif test.operation == gx.AOP_OR:
+ never_pass = test.function0 == test.function1 == gx.CompareFunction.NEVER
+ always_pass = test.function0 == gx.CompareFunction.ALWAYS or test.function1 == gx.CompareFunction.ALWAYS
+ elif test.operation == gx.AOP_XOR:
+ never_pass = test.function0 == test.function1 in {gx.CompareFunction.NEVER,gx.CompareFunction.ALWAYS}
+ always_pass = {test.function0,test.function1} == {gx.CompareFunction.NEVER,gx.CompareFunction.ALWAYS}
+ elif test.operation == gx.AOP_XNOR:
+ never_pass = {test.function0,test.function1} == {gx.CompareFunction.NEVER,gx.CompareFunction.ALWAYS}
+ always_pass = test.function0 == test.function1 in {gx.CompareFunction.NEVER,gx.CompareFunction.ALWAYS}
+
+ if always_pass:
+ return
+
+ if never_pass:
+ stream.write('discard;\n')
+ return
+
+ comparison0 = convert_alpha_comparison(test.function0,test.reference0)
+ comparison1 = convert_alpha_comparison(test.function1,test.reference1)
+ operation = convert_alpha_test_operation(test.operation)
+ stream.write('if ( !(({}) {} ({})) ){{\n'.format(comparison0,operation,comparison1))
+ stream.write(' discard;}\n')
+
+
+def create_shader_string(material):
+ stream = StringIO()
+
+ stream.write('#version 330\n')
+
+ if material.depth_test_early and glInitShaderImageLoadStoreARB():
+ # This doesn't force the driver to write to the depth buffer
+ # if the alpha test fails, it just allows it
+ stream.write('#extension GL_ARB_shader_image_load_store : enable\n')
+ stream.write('layout(early_fragment_tests) in;\n')
+
+ stream.write('{}\n'.format(material.gl_block.glsl_type))
+
+ for i in range(material.channel_count):
+ stream.write('in vec4 channel{};\n'.format(i))
+
+ for i,generator in enumerate(material.enabled_texcoord_generators):
+ if generator.function == gx.TG_MTX3x4:
+ stream.write('in vec3 generated_texcoord{};\n'.format(i))
+ else:
+ stream.write('in vec2 generated_texcoord{};\n'.format(i))
+
+ for i in range(8):
+ if not material.use_texture[i]: continue
+ stream.write('uniform sampler2D texmap{};\n'.format(i))
+
+ stream.write('\nvoid main()\n{\n')
+ stream.write('float alphabump;\n')
+ stream.write('vec3 indtevcrd;\n')
+ stream.write('vec2 indtevtrans;\n')
+ stream.write('vec2 wrappedcoord;\n')
+ stream.write('vec2 tevcoord;\n')
+ stream.write('vec4 textemp;\n')
+
+ for i,generator in enumerate(material.enabled_texcoord_generators):
+ stream.write('vec2 uv{} = generated_texcoord{}'.format(i,i))
+ if generator.function == gx.TG_MTX3x4:
+ stream.write('.st/generated_texcoord{}.p'.format(i))
+ stream.write(';\n')
+
+ stream.write('vec4 tevprev = tev_color_previous;\n')
+
+ for i in range(3):
+ stream.write('vec4 tevreg{} = tev_color{};\n'.format(i,i))
+
+ for i,stage in enumerate(material.enabled_indirect_stages):
+ write_indirect_stage(stream,i,stage)
+
+ for i,stage in enumerate(material.enabled_tev_stages):
+ stream.write('\n// TEV stage {}\n'.format(i))
+ write_tev_stage(stream,stage,material)
+
+ stream.write('tevprev = fract(tevprev*(255.0/256.0))*(256.0/255.0);\n\n')
+
+ write_alpha_test(stream,material.alpha_test)
+
+ stream.write('gl_FragColor = tevprev;\n')
+ stream.write('}\n')
+
+ return stream.getvalue()
+
diff --git a/j3d/inf1.py b/j3d/inf1.py
new file mode 100644
index 0000000..022203e
--- /dev/null
+++ b/j3d/inf1.py
@@ -0,0 +1,107 @@
+from enum import IntEnum
+from btypes.big_endian import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ unknown0 = uint16
+ __padding__ = Padding(2)
+ shape_batch_count = uint32
+ vertex_position_count = uint32
+ scene_graph_offset = uint32
+
+ def __init__(self):
+ self.magic = b'INF1'
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'INF1':
+ raise FormatError('invalid magic')
+ if header.unknown0 not in {0,1,2}:
+ logger.warning('unknown0 different from default')
+ return header
+
+
+class NodeType(IntEnum):
+ END_GRAPH = 0x00
+ BEGIN_CHILDREN = 0x01
+ END_CHILDREN = 0x02
+ JOINT = 0x10
+ MATERIAL = 0x11
+ SHAPE = 0x12
+
+
+class Node(Struct):
+ node_type = EnumConverter(uint16,NodeType)
+ index = uint16
+
+ def __init__(self,node_type,index):
+ self.node_type = node_type
+ self.index = index
+ self.children = []
+
+
+class SceneGraph:
+
+ def __init__(self):
+ self.unknown0 = 0
+ self.children = []
+
+
+def pack_children(stream,parent):
+ for node in parent.children:
+ Node.pack(stream,node)
+ if node.children:
+ Node.pack(stream,Node(NodeType.BEGIN_CHILDREN,0))
+ pack_children(stream,node)
+ Node.pack(stream,Node(NodeType.END_CHILDREN,0))
+
+
+def unpack_children(stream,parent,end_node_type=NodeType.END_CHILDREN):
+ while True:
+ node = Node.unpack(stream)
+ if node.node_type == end_node_type:
+ break
+ elif node.node_type == NodeType.BEGIN_CHILDREN:
+ unpack_children(stream,parent.children[-1])
+ else:
+ node.children = []
+ parent.children.append(node)
+
+
+def pack(stream,scene_graph,shape_batch_count,vertex_position_count):
+ base = stream.tell()
+ header = Header()
+ header.unknown0 = scene_graph.unknown0
+ header.shape_batch_count = shape_batch_count
+ header.vertex_position_count = vertex_position_count
+ stream.write(b'\x00'*Header.sizeof())
+
+ header.scene_graph_offset = stream.tell() - base
+ pack_children(stream,scene_graph)
+ Node.pack(stream,Node(NodeType.END_GRAPH,0))
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+ stream.seek(base)
+ Header.pack(stream,header)
+ stream.seek(base + header.section_size)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+
+ stream.seek(base + header.scene_graph_offset)
+ scene_graph = SceneGraph()
+ scene_graph.unknown0 = header.unknown0
+ unpack_children(stream,scene_graph,NodeType.END_GRAPH)
+
+ stream.seek(base + header.section_size)
+ return scene_graph,header.shape_batch_count,header.vertex_position_count
+
diff --git a/j3d/jnt1.py b/j3d/jnt1.py
new file mode 100644
index 0000000..392c7e2
--- /dev/null
+++ b/j3d/jnt1.py
@@ -0,0 +1,165 @@
+from math import cos,sin,radians
+import numpy
+from btypes.big_endian import *
+import j3d.string_table
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ joint_count = uint16
+ __padding__ = Padding(2)
+ joint_offset = uint32
+ index_offset = uint32
+ name_offset = uint32
+
+ def __init__(self):
+ self.magic = b'JNT1'
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'JNT1':
+ raise FormatError('invalid magic')
+ return header
+
+
+def matrix3x4_multiply(a,b):
+ c = numpy.empty((3,4),numpy.float32)
+ c[:,0:3] = numpy.dot(a[:,0:3],b[:,0:3])
+ c[:,3] = numpy.dot(a[:,0:3],b[:,3])
+ c[:,3] += a[:,3]
+ return c
+
+
+class Joint(Struct):
+ # 0 -> has direct material/shape descendant
+ # 2 -> only referenced by other joints
+ unknown0 = uint16
+ ignore_parent_scale = bool8
+ __padding__ = Padding(1)
+ scale_x = float32
+ scale_y = float32
+ scale_z = float32
+ rotation_x = FixedPointConverter(sint16,180/32767)
+ rotation_y = FixedPointConverter(sint16,180/32767)
+ rotation_z = FixedPointConverter(sint16,180/32767)
+ __padding__ = Padding(2)
+ translation_x = float32
+ translation_y = float32
+ translation_z = float32
+ bounding_radius = float32
+ min_x = float32
+ min_y = float32
+ min_z = float32
+ max_x = float32
+ max_y = float32
+ max_z = float32
+
+ def __init__(self):
+ self.unknown0 = 0
+ self.ignore_parent_scale = False
+ self.scale_x = 1
+ self.scale_y = 1
+ self.scale_z = 1
+ self.rotation_x = 0
+ self.rotation_y = 0
+ self.rotation_z = 0
+ self.translation_x = 0
+ self.translation_y = 0
+ self.translation_z = 0
+
+ @classmethod
+ def unpack(cls,stream):
+ joint = super().unpack(stream)
+ if joint.unknown0 not in {0,1,2}:
+ logger.warning('unknown0 different from default')
+ return joint
+
+ def create_matrix(self,parent_joint,parent_joint_matrix):
+ # The calculation of the local matrix is an optimized version of
+ # local_matrix = T*IPS*R*S if ignore_parent_scale else T*R*S
+ # where S, R and T is the scale, rotation and translation matrix
+ # respectively and IPS is the inverse parent scale matrix.
+
+ cx = cos(radians(self.rotation_x))
+ sx = sin(radians(self.rotation_x))
+ cy = cos(radians(self.rotation_y))
+ sy = sin(radians(self.rotation_y))
+ cz = cos(radians(self.rotation_z))
+ sz = sin(radians(self.rotation_z))
+
+ if self.ignore_parent_scale:
+ ips_x = 1/parent_joint.scale_x
+ ips_y = 1/parent_joint.scale_y
+ ips_z = 1/parent_joint.scale_z
+ else:
+ ips_x = 1
+ ips_y = 1
+ ips_z = 1
+
+ local_matrix = numpy.empty((3,4),numpy.float32)
+ local_matrix[0,0] = cy*cz*self.scale_x*ips_x
+ local_matrix[1,0] = cy*sz*self.scale_x*ips_y
+ local_matrix[2,0] = -sy*self.scale_x*ips_z
+ local_matrix[0,1] = (sx*sy*cz - cx*sz)*self.scale_y*ips_x
+ local_matrix[1,1] = (sx*sy*sz + cx*cz)*self.scale_y*ips_y
+ local_matrix[2,1] = sx*cy*self.scale_y*ips_z
+ local_matrix[0,2] = (cx*sy*cz + sx*sz)*self.scale_z*ips_x
+ local_matrix[1,2] = (cx*sy*sz - sx*cz)*self.scale_z*ips_y
+ local_matrix[2,2] = cx*cy*self.scale_z*ips_z
+ local_matrix[0,3] = self.translation_x
+ local_matrix[1,3] = self.translation_y
+ local_matrix[2,3] = self.translation_z
+
+ return matrix3x4_multiply(parent_joint_matrix,local_matrix)
+
+
+def pack(stream,joints):
+ base = stream.tell()
+ header = Header()
+ header.joint_count = len(joints)
+ stream.write(b'\x00'*Header.sizeof())
+
+ header.joint_offset = stream.tell() - base
+ for joint in joints:
+ Joint.pack(stream,joint)
+
+ header.index_offset = stream.tell() - base
+ for index in range(len(joints)):
+ uint16.pack(stream,index)
+
+ align(stream,4)
+ header.name_offset = stream.tell() - base
+ j3d.string_table.pack(stream,(joint.name for joint in joints))
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+ stream.seek(base)
+ Header.pack(stream,header)
+ stream.seek(base + header.section_size)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+
+ stream.seek(base + header.joint_offset)
+ joints = [Joint.unpack(stream) for _ in range(header.joint_count)]
+
+ stream.seek(base + header.index_offset)
+ for index in range(header.joint_count):
+ if index != uint16.unpack(stream):
+ raise FormatError('invalid index')
+
+ stream.seek(base + header.name_offset)
+ names = j3d.string_table.unpack(stream)
+ for joint,name in zip(joints,names):
+ joint.name = name
+
+ stream.seek(base + header.section_size)
+ return joints
+
diff --git a/j3d/mat3.py b/j3d/mat3.py
new file mode 100644
index 0000000..59642a4
--- /dev/null
+++ b/j3d/mat3.py
@@ -0,0 +1,1060 @@
+import functools
+from btypes.big_endian import *
+import gx
+from j3d.material import *
+import j3d.string_table
+
+import logging
+logger = logging.getLogger(__name__)
+
+index8 = NoneableConverter(uint8,0xFF)
+index16 = NoneableConverter(uint16,0xFFFF)
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ material_count = uint16
+ __padding__ = Padding(2)
+ entry_offset = uint32
+ entry_index_offset = uint32
+ name_offset = uint32
+ indirect_entry_offset = uint32
+ cull_mode_offset = uint32
+ material_color_offset = uint32
+ channel_count_offset = uint32
+ lighting_mode_offset = uint32
+ ambient_color_offset = uint32
+ light_offset = uint32
+ texcoord_generator_count_offset = uint32
+ texcoord_generator_offset = uint32
+ unknown2_offset = uint32
+ texture_matrix_offset = uint32
+ unknown3_offset = uint32
+ texture_index_offset = uint32
+ tev_order_offset = uint32
+ tev_color_offset = uint32
+ kcolor_offset = uint32
+ tev_stage_count_offset = uint32
+ tev_combiner_offset = uint32
+ swap_mode_offset = uint32
+ swap_table_offset = uint32
+ fog_offset = uint32
+ alpha_test_offset = uint32
+ blend_mode_offset = uint32
+ depth_mode_offset = uint32
+ depth_test_early_offset = uint32
+ dither_offset = uint32
+ unknown5_offset = uint32
+
+ def __init__(self):
+ self.magic = b'MAT3'
+ self.unknown2_offset = 0
+ self.unknown3_offset = 0
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'MAT3':
+ raise FormatError('invalid magic')
+ if header.unknown2_offset != 0:
+ logger.warning('unknown2_offset different from default')
+ if header.unknown3_offset != 0:
+ logger.warning('unknown3_offset different from default')
+ return header
+
+
+class ColorS16(Color,replace_fields=True):
+ r = sint16
+ g = sint16
+ b = sint16
+ a = sint16
+
+
+class TevCombiner(Struct):
+ unknown0 = uint8
+ color_mode = TevColorMode
+ alpha_mode = TevAlphaMode
+ unknown1 = uint8
+
+ @classmethod
+ def unpack(cls,stream):
+ tev_combiner = super().unpack(stream)
+ if tev_combiner.unknown0 != 0xFF:
+ logger.warning('tev combiner unknown0 different from default')
+ if tev_combiner.unknown1 != 0xFF:
+ logger.warning('tev combiner unknown1 different from default')
+ return tev_combiner
+
+
+class TevOrder(Struct):
+ """Arguments to GXSetTevOrder."""
+ texcoord = EnumConverter(uint8,gx.TexCoord)
+ texture = EnumConverter(uint8,gx.Texture)
+ color = EnumConverter(uint8,gx.Channel)
+ __padding__ = Padding(1)
+
+
+class SwapMode(Struct):
+ """Arguments to GXSetTevSwapMode."""
+ color_swap_table = EnumConverter(uint8,gx.SwapTable)
+ texture_swap_table = EnumConverter(uint8,gx.SwapTable)
+ __padding__ = Padding(2)
+
+
+class TevIndirect(Struct):
+ """Arguments to GXSetTevIndirect."""
+ indirect_stage = EnumConverter(uint8,gx.IndirectStage)
+ indirect_format = EnumConverter(uint8,gx.IndirectFormat)
+ indirect_bias_components = EnumConverter(uint8,gx.IndirectBiasComponents)
+ indirect_matrix = EnumConverter(uint8,gx.IndirectMatrix)
+ wrap_s = EnumConverter(uint8,gx.IndirectWrap)
+ wrap_t = EnumConverter(uint8,gx.IndirectWrap)
+ add_previous_texcoord = bool8
+ use_original_lod = bool8
+ bump_alpha = EnumConverter(uint8,gx.IndirectBumpAlpha)
+ __padding__ = Padding(3)
+
+
+class IndirectOrder(Struct):
+ """Arguments to GXSetIndTexOrder."""
+ texcoord = EnumConverter(uint8,gx.TexCoord)
+ texture = EnumConverter(uint8,gx.Texture)
+ __padding__ = Padding(2)
+
+
+class IndirectTexCoordScale(Struct):
+ """Arguments to GXSetIndTexCoordScale."""
+ scale_s = EnumConverter(uint8,gx.IndirectScale)
+ scale_t = EnumConverter(uint8,gx.IndirectScale)
+ __padding__ = Padding(2)
+
+
+class ChannelEntry(Struct):
+ color_mode_index = index16
+ alpha_mode_index = index16
+
+ def __init__(self):
+ self.color_mode_index = None
+ self.alpha_mode_index = None
+
+
+class Entry(Struct):
+ unknown0 = uint8
+ cull_mode_index = index8
+ channel_count_index = index8
+ texcoord_generator_count_index = index8
+ tev_stage_count_index = index8
+ depth_test_early_index = index8
+ depth_mode_index = index8
+ dither_index = index8
+ material_color_indices = Array(index16,2)
+ channels = Array(ChannelEntry,2)
+ ambient_color_indices = Array(index16,2)
+ light_indices = Array(index16,8)
+ texcoord_generator_indices = Array(index16,8)
+ unknown2 = Array(uint16,8)
+ texture_matrix_indices = Array(index16,10)
+ unknown3 = Array(uint16,20)
+ texture_index_indices = Array(index16,8)
+ kcolor_indices = Array(index16,4)
+ constant_colors = Array(EnumConverter(uint8,gx.ConstantColor),16)
+ constant_alphas = Array(EnumConverter(uint8,gx.ConstantAlpha),16)
+ tev_order_indices = Array(index16,16)
+ tev_color_indices = Array(index16,3)
+ tev_color_previous_index = index16
+ tev_combiner_indices = Array(index16,16)
+ swap_mode_indices = Array(index16,16)
+ swap_table_indices = Array(index16,4)
+ unknown4 = Array(uint16,12)
+ fog_index = index16
+ alpha_test_index = index16
+ blend_mode_index = index16
+ unknown5_index = index16
+
+ def __init__(self):
+ self.unknown0 = 1
+ self.cull_mode_index = None
+ self.channel_count_index = None
+ self.texcoord_generator_count_index = None
+ self.tev_stage_count_index = None
+ self.depth_test_early_index = None
+ self.depth_mode_index = None
+ self.dither_index = None
+ self.material_color_indices = [None]*2
+ self.channels = [ChannelEntry() for _ in range(2)]
+ self.ambient_color_indices = [None]*2
+ self.light_indices = [None]*8
+ self.texcoord_generator_indices = [None]*8
+ self.unknown2 = [0xFFFF]*8
+ self.texture_matrix_indices = [None]*10
+ self.unknown3 = [0xFFFF]*20
+ self.texture_index_indices = [None]*8
+ self.kcolor_indices = [None]*4
+ self.constant_colors = [gx.TEV_KCSEL_1]*16
+ self.constant_alphas = [gx.TEV_KASEL_1]*16
+ self.tev_order_indices = [None]*16
+ self.tev_color_indices = [None]*3
+ self.tev_color_previous_index = None
+ self.tev_combiner_indices = [None]*16
+ self.swap_mode_indices = [None]*16
+ self.swap_table_indices = [None]*4
+ self.unknown4 = [0xFFFF]*12
+ self.fog_index = None
+ self.alpha_test_index = None
+ self.blend_mode_index = None
+ self.unknown5_index = None
+
+ @classmethod
+ def unpack(cls,stream):
+ entry = super().unpack(stream)
+ if entry.unknown2 != [0xFFFF]*8:
+ logger.warning('unknown2 different from default')
+ return entry
+
+ def load_constant_colors(self,material):
+ for i,stage in enumerate(material.tev_stages):
+ self.constant_colors[i] = stage.constant_color
+
+ def load_constant_alphas(self,material):
+ for i,stage in enumerate(material.tev_stages):
+ self.constant_alphas[i] = stage.constant_alpha
+
+ def unload_constant_colors(self,material):
+ for stage,constant_color in zip(material.tev_stages,self.constant_colors):
+ stage.constant_color = constant_color
+
+ def unload_constant_alphas(self,material):
+ for stage,constant_alpha in zip(material.tev_stages,self.constant_alphas):
+ stage.constant_alpha = constant_alpha
+
+
+
+class IndirectEntry(Struct):
+ unknown0 = uint8 # enable or indirect stage count?
+ unknown1 = uint8 # enable or indirect stage count?
+ __padding__ = Padding(2)
+ indirect_orders = Array(IndirectOrder,4)
+ indirect_matrices = Array(IndirectMatrix,3)
+ indirect_texcoord_scales = Array(IndirectTexCoordScale,4)
+ tev_indirects = Array(TevIndirect,16)
+
+ def __init__(self):
+ self.tev_indirects = [TevIndirect() for _ in range(16)]
+ self.indirect_orders = [IndirectOrder() for _ in range(4)]
+ self.indirect_texcoord_scales = [IndirectTexCoordScale() for _ in range(4)]
+ self.indirect_matrices = [IndirectMatrix() for _ in range(3)]
+
+ @classmethod
+ def unpack(cls,stream):
+ indirect_entry = super().unpack(stream)
+ if indirect_entry.unknown0 != indirect_entry.unknown1 or indirect_entry.unknown0 not in {0,1}:
+ raise FormatError('unsuported indirect texture entry unknown0 and unknown1')
+ return indirect_entry
+
+ def load(self,material):
+ self.unknown0 = material.indirect_stage_count
+ self.unknown1 = material.indirect_stage_count
+
+ for stage,tev_indirect in zip(material.tev_stages,self.tev_indirects):
+ tev_indirect.indirect_stage = stage.indirect_stage
+ tev_indirect.indirect_format = stage.indirect_format
+ tev_indirect.indirect_bias_components = stage.indirect_bias_components
+ tev_indirect.indirect_matrix = stage.indirect_matrix
+ tev_indirect.wrap_s = stage.wrap_s
+ tev_indirect.wrap_t = stage.wrap_t
+ tev_indirect.add_previous_texcoord = stage.add_previous_texcoord
+ tev_indirect.use_original_lod = stage.use_original_lod
+ tev_indirect.bump_alpha = stage.bump_alpha
+
+ for stage,order in zip(material.indirect_stages,self.indirect_orders):
+ order.texcoord = stage.texcoord
+ order.texture = stage.texture
+
+ for stage,texcoord_scale in zip(material.indirect_stages,self.indirect_texcoord_scales):
+ texcoord_scale.scale_s = stage.scale_s
+ texcoord_scale.scale_t = stage.scale_t
+
+ self.indirect_matrices = material.indirect_matrices
+
+ def unload(self,material):
+ material.indirect_stage_count = self.unknown0
+
+ for tev_stage,tev_indirect in zip(material.tev_stages,self.tev_indirects):
+ tev_stage.indirect_stage = tev_indirect.indirect_stage
+ tev_stage.indirect_format = tev_indirect.indirect_format
+ tev_stage.indirect_bias_components = tev_indirect.indirect_bias_components
+ tev_stage.indirect_matrix = tev_indirect.indirect_matrix
+ tev_stage.wrap_s = tev_indirect.wrap_s
+ tev_stage.wrap_t = tev_indirect.wrap_t
+ tev_stage.add_previous_texcoord = tev_indirect.add_previous_texcoord
+ tev_stage.use_original_lod = tev_indirect.use_original_lod
+ tev_stage.bump_alpha = tev_indirect.bump_alpha
+
+ for stage,order in zip(material.indirect_stages,self.indirect_orders):
+ stage.texcoord = order.texcoord
+ stage.texture = order.texture
+
+ for stage,texcoord_scale in zip(material.indirect_stages,self.indirect_texcoord_scales):
+ stage.scale_s = texcoord_scale.scale_s
+ stage.scale_t = texcoord_scale.scale_t
+
+ material.indirect_matrices = self.indirect_matrices
+
+
+class Pool:
+
+ def __init__(self,element_type,values=tuple(),equal_predicate=None):
+ self.element_type = element_type
+ self.values = list(values)
+ if equal_predicate is not None:
+ self.equal_predicate = equal_predicate
+
+ def __getitem__(self,value):
+ for i in range(len(self.values)):
+ if self.equal_predicate(value,self.values[i]):
+ return i
+
+ self.values.append(value)
+ return len(self.values) - 1
+
+ def __iter__(self):
+ yield from self.values
+
+ @staticmethod
+ def equal_predicate(a,b):
+ return a == b
+
+
+class ArrayUnpacker:
+
+ def __init__(self,stream,offset,element_type):
+ self.stream = stream
+ self.offset = offset
+ self.element_type = element_type
+
+ def __getitem__(self,index):
+ self.stream.seek(self.offset + index*self.element_type.sizeof())
+ return self.element_type.unpack(self.stream)
+
+
+def partial_call(func):
+ def wrapper(*args,**kwargs):
+ return functools.partial(func,*args,**kwargs)
+ return wrapper
+
+
+@partial_call
+def pool_loader(element_type,load_function,**kwargs):
+ @functools.wraps(load_function)
+ def wrapper(self,materials,entries):
+ pool = Pool(element_type,**kwargs)
+ for material,entry in zip(materials,entries):
+ load_function(pool,material,entry)
+ return pool
+ return wrapper
+
+
+@partial_call
+def array_unloader(element_type,unload_function):
+ @functools.wraps(unload_function)
+ def wrapper(self,offset,materials,entries):
+ array = self.create_array(offset,element_type)
+ for material,entry in zip(materials,entries):
+ unload_function(array,material,entry)
+ return wrapper
+
+
+def equal_tev_combiner_and_swap_mode(a,b):
+ return TevCombiner.__eq__(a,b) and SwapMode.__eq__(a,b)
+
+
+class SectionPacker:
+
+ entry_type = Entry
+
+ def seek(self,offset):
+ self.stream.seek(self.base + offset)
+
+ def tell(self):
+ return self.stream.tell() - self.base
+
+ def pack(self,stream,materials):
+ self.stream = stream
+ self.base = stream.tell()
+
+ entries = [self.entry_type() for _ in range(len(materials))]
+
+ for material,entry in zip(materials,entries):
+ entry.unknown0 = material.unknown0
+ entry.unknown2 = material.unknown2
+ entry.unknown3 = material.unknown3
+ entry.unknown4 = material.unknown4
+ entry.load_constant_colors(material)
+ entry.load_constant_alphas(material)
+
+ cull_mode_pool = self.pool_cull_mode(materials,entries)
+ channel_count_pool = self.pool_channel_count(materials,entries)
+ material_color_pool = self.pool_material_color(materials,entries)
+ ambient_color_pool = self.pool_ambient_color(materials,entries)
+ lighting_mode_pool = self.pool_lighting_mode(materials,entries)
+ light_pool = self.pool_light(materials,entries)
+ texcoord_generator_count_pool = self.pool_texcoord_generator_count(materials,entries)
+ texcoord_generator_pool = self.pool_texcoord_generator(materials,entries)
+ texture_matrix_pool = self.pool_texture_matrix(materials,entries)
+ texture_index_pool = self.pool_texture_index(materials,entries)
+ tev_stage_count_pool = self.pool_tev_stage_count(materials,entries)
+ tev_order_pool = self.pool_tev_order(materials,entries)
+ tev_combiner_pool = self.pool_tev_combiner(materials,entries)
+ swap_mode_pool = self.pool_swap_mode(materials,entries)
+ tev_color_pool = self.pool_tev_color(materials,entries)
+ kcolor_pool = self.pool_kcolor(materials,entries)
+ swap_table_pool = self.pool_swap_table(materials,entries)
+ fog_pool = self.pool_fog(materials,entries)
+ alpha_test_pool = self.pool_alpha_test(materials,entries)
+ blend_mode_pool = self.pool_blend_mode(materials,entries)
+ depth_mode_pool = self.pool_depth_mode(materials,entries)
+ depth_test_early_pool = self.pool_depth_test_early(materials,entries)
+ dither_pool = self.pool_dither(materials,entries)
+ unknown5_pool = self.pool_unknown5(materials,entries)
+
+ entry_pool = Pool(self.entry_type)
+ entry_indices = [entry_pool[entry] for entry in entries]
+
+ header = Header()
+ header.material_count = len(materials)
+ stream.write(b'\x00'*Header.sizeof())
+
+ header.entry_offset = self.pack_pool(entry_pool)
+
+ header.entry_index_offset = self.tell()
+ for index in entry_indices:
+ uint16.pack(stream,index)
+
+ align(stream,4)
+ header.name_offset = self.tell()
+ j3d.string_table.pack(stream,(material.name for material in materials))
+
+ align(stream,4)
+ header.indirect_entry_offset = self.pack_indirect_entries(materials)
+
+ align(stream,4)
+ header.cull_mode_offset = self.pack_pool(cull_mode_pool)
+ header.material_color_offset = self.pack_pool(material_color_pool)
+ header.channel_count_offset = self.pack_pool(channel_count_pool)
+ align(stream,4)
+ header.lighting_mode_offset = self.pack_pool(lighting_mode_pool)
+ header.ambient_color_offset = self.pack_pool(ambient_color_pool)
+ header.light_offset = self.pack_pool(light_pool)
+ header.texcoord_generator_count_offset = self.pack_pool(texcoord_generator_count_pool)
+ align(stream,4)
+ header.texcoord_generator_offset = self.pack_pool(texcoord_generator_pool)
+ header.texture_matrix_offset = self.pack_pool(texture_matrix_pool)
+ header.texture_index_offset = self.pack_pool(texture_index_pool)
+ align(stream,4)
+ header.tev_order_offset = self.pack_pool(tev_order_pool)
+ header.tev_color_offset = self.pack_pool(tev_color_pool)
+ header.kcolor_offset = self.pack_pool(kcolor_pool)
+ header.tev_stage_count_offset = self.pack_pool(tev_stage_count_pool)
+ align(stream,4)
+ header.tev_combiner_offset = self.pack_pool(tev_combiner_pool)
+ header.swap_mode_offset = self.pack_pool(swap_mode_pool)
+ header.swap_table_offset = self.pack_pool(swap_table_pool)
+ header.fog_offset = self.pack_pool(fog_pool)
+ header.alpha_test_offset = self.pack_pool(alpha_test_pool)
+ header.blend_mode_offset = self.pack_pool(blend_mode_pool)
+ header.depth_mode_offset = self.pack_pool(depth_mode_pool)
+ header.depth_test_early_offset = self.pack_pool(depth_test_early_pool)
+ align(stream,4)
+ header.dither_offset = self.pack_pool(dither_pool)
+ align(stream,4)
+ header.unknown5_offset = self.pack_pool(unknown5_pool)
+
+ align(stream,0x20)
+ header.section_size = self.tell()
+ self.seek(0)
+ Header.pack(stream,header)
+ self.seek(header.section_size)
+
+ def pack_indirect_entries(self,materials):
+ offset = self.tell()
+ for material in materials:
+ indirect_entry = IndirectEntry()
+ indirect_entry.load(material)
+ IndirectEntry.pack(self.stream,indirect_entry)
+ return offset
+
+ def pack_pool(self,pool):
+ if pool is None: return 0
+ offset = self.tell()
+ for value in pool:
+ pool.element_type.pack(self.stream,value)
+ return offset
+
+ @pool_loader(EnumConverter(uint32,gx.CullMode),values=(gx.CULL_BACK,gx.CULL_FRONT,gx.CULL_NONE))
+ def pool_cull_mode(pool,material,entry):
+ entry.cull_mode_index = pool[material.cull_mode]
+
+ @pool_loader(uint8)
+ def pool_channel_count(pool,material,entry):
+ entry.channel_count_index = pool[material.channel_count]
+
+ @pool_loader(Color)
+ def pool_material_color(pool,material,entry):
+ for i,channel in enumerate(material.channels):
+ entry.material_color_indices[i] = pool[channel.material_color]
+
+ @pool_loader(Color)
+ def pool_ambient_color(pool,material,entry):
+ for i,channel in enumerate(material.channels):
+ entry.ambient_color_indices[i] = pool[channel.ambient_color]
+
+ @pool_loader(LightingMode)
+ def pool_lighting_mode(pool,material,entry):
+ for channel,channel_entry in zip(material.channels,entry.channels):
+ channel_entry.color_mode_index = pool[channel.color_mode]
+ channel_entry.alpha_mode_index = pool[channel.alpha_mode]
+
+ @pool_loader(Light)
+ def pool_light(pool,material,entry):
+ for i,light in enumerate(material.lights):
+ if light is None: continue
+ entry.light_indices[i] = pool[light]
+
+ @pool_loader(uint8)
+ def pool_texcoord_generator_count(pool,material,entry):
+ entry.texcoord_generator_count_index = pool[material.texcoord_generator_count]
+
+ @pool_loader(TexCoordGenerator)
+ def pool_texcoord_generator(pool,material,entry):
+ for i,generator in enumerate(material.enabled_texcoord_generators):
+ entry.texcoord_generator_indices[i] = pool[generator]
+
+ @pool_loader(TextureMatrix)
+ def pool_texture_matrix(pool,material,entry):
+ for i,matrix in enumerate(material.texture_matrices):
+ if matrix is None: continue
+ entry.texture_matrix_indices[i] = pool[matrix]
+
+ @pool_loader(uint16)
+ def pool_texture_index(pool,material,entry):
+ for i,index in enumerate(material.texture_indices):
+ if index is None: continue
+ entry.texture_index_indices[i] = pool[index]
+
+ @pool_loader(uint8)
+ def pool_tev_stage_count(pool,material,entry):
+ entry.tev_stage_count_index = pool[material.tev_stage_count]
+
+ @pool_loader(TevOrder,equal_predicate=TevOrder.__eq__)
+ def pool_tev_order(pool,material,entry):
+ for i,stage in enumerate(material.enabled_tev_stages):
+ entry.tev_order_indices[i] = pool[stage]
+
+ @pool_loader(TevCombiner,equal_predicate=equal_tev_combiner_and_swap_mode)
+ def pool_tev_combiner(pool,material,entry):
+ for i,stage in enumerate(material.enabled_tev_stages):
+ entry.tev_combiner_indices[i] = pool[stage]
+
+ @pool_loader(SwapMode,equal_predicate=equal_tev_combiner_and_swap_mode)
+ def pool_swap_mode(pool,material,entry):
+ for i,stage in enumerate(material.enabled_tev_stages):
+ entry.swap_mode_indices[i] = pool[stage]
+
+ @pool_loader(ColorS16)
+ def pool_tev_color(pool,material,entry):
+ for i,color in enumerate(material.tev_colors):
+ entry.tev_color_indices[i] = pool[color]
+
+ entry.tev_color_previous_index = pool[material.tev_color_previous]
+
+ @pool_loader(Color)
+ def pool_kcolor(pool,material,entry):
+ for i,color in enumerate(material.kcolors):
+ entry.kcolor_indices[i] = pool[color]
+
+ @pool_loader(SwapTable)
+ def pool_swap_table(pool,material,entry):
+ for i,table in enumerate(material.swap_tables):
+ entry.swap_table_indices[i] = pool[table]
+
+ @pool_loader(Fog)
+ def pool_fog(pool,material,entry):
+ entry.fog_index = pool[material.fog]
+
+ @pool_loader(AlphaTest)
+ def pool_alpha_test(pool,material,entry):
+ entry.alpha_test_index = pool[material.alpha_test]
+
+ @pool_loader(BlendMode)
+ def pool_blend_mode(pool,material,entry):
+ entry.blend_mode_index = pool[material.blend_mode]
+
+ @pool_loader(DepthMode)
+ def pool_depth_mode(pool,material,entry):
+ entry.depth_mode_index = pool[material.depth_mode]
+
+ @pool_loader(bool8,values=(False,True))
+ def pool_depth_test_early(pool,material,entry):
+ entry.depth_test_early_index = pool[material.depth_test_early]
+
+ @pool_loader(bool8,values=(False,True))
+ def pool_dither(pool,material,entry):
+ entry.dither_index = pool[material.dither]
+
+ @pool_loader(UnknownStruct5)
+ def pool_unknown5(pool,material,entry):
+ entry.unknown5_index = pool[material.unknown5]
+
+
+class SectionUnpacker:
+
+ entry_type = Entry
+
+ def seek(self,offset):
+ self.stream.seek(self.base + offset)
+
+ def unpack(self,stream):
+ self.stream = stream
+ self.base = stream.tell()
+
+ header = Header.unpack(stream)
+
+ materials = [Material() for _ in range(header.material_count)]
+
+ self.seek(header.entry_index_offset)
+ entry_indices = [uint16.unpack(stream) for _ in range(header.material_count)]
+
+ entry_count = max(entry_indices) + 1
+ self.seek(header.entry_offset)
+ entries = [self.entry_type.unpack(stream) for _ in range(entry_count)]
+ entries = [entries[i] for i in entry_indices]
+
+ for material,entry in zip(materials,entries):
+ material.unknown0 = entry.unknown0
+ material.unknown2 = entry.unknown2
+ material.unknown3 = entry.unknown3
+ material.unknown4 = entry.unknown4
+ entry.unload_constant_colors(material)
+ entry.unload_constant_alphas(material)
+
+ self.seek(header.name_offset)
+ names = j3d.string_table.unpack(stream)
+ for material,name in zip(materials,names):
+ material.name = name
+
+ self.unpack_indirect_entries(header.indirect_entry_offset,materials)
+
+ self.unpack_cull_mode(header.cull_mode_offset,materials,entries)
+ self.unpack_channel_count(header.channel_count_offset,materials,entries)
+ self.unpack_material_color(header.material_color_offset,materials,entries)
+ self.unpack_ambient_color(header.ambient_color_offset,materials,entries)
+ self.unpack_lighting_mode(header.lighting_mode_offset,materials,entries)
+ self.unpack_light(header.light_offset,materials,entries)
+ self.unpack_texcoord_generator_count(header.texcoord_generator_count_offset,materials,entries)
+ self.unpack_texcoord_generator(header.texcoord_generator_offset,materials,entries)
+ self.unpack_texture_matrix(header.texture_matrix_offset,materials,entries)
+ self.unpack_texture_index(header.texture_index_offset,materials,entries)
+ self.unpack_tev_stage_count(header.tev_stage_count_offset,materials,entries)
+ self.unpack_tev_order(header.tev_order_offset,materials,entries)
+ self.unpack_tev_combiner(header.tev_combiner_offset,materials,entries)
+ self.unpack_swap_mode(header.swap_mode_offset,materials,entries)
+ self.unpack_tev_color(header.tev_color_offset,materials,entries)
+ self.unpack_kcolor(header.kcolor_offset,materials,entries)
+ self.unpack_swap_table(header.swap_table_offset,materials,entries)
+ self.unpack_fog(header.fog_offset,materials,entries)
+ self.unpack_alpha_test(header.alpha_test_offset,materials,entries)
+ self.unpack_blend_mode(header.blend_mode_offset,materials,entries)
+ self.unpack_depth_mode(header.depth_mode_offset,materials,entries)
+ self.unpack_depth_test_early(header.depth_test_early_offset,materials,entries)
+ self.unpack_dither(header.dither_offset,materials,entries)
+ self.unpack_unknown5(header.unknown5_offset,materials,entries)
+
+ self.seek(header.section_size)
+ return materials
+
+ def unpack_indirect_entries(self,offset,materials):
+ self.seek(offset)
+ for material in materials:
+ indirect_entry = IndirectEntry.unpack(self.stream)
+ indirect_entry.unload(material)
+
+ def create_array(self,offset,element_type):
+ if offset == 0: return None
+ return ArrayUnpacker(self.stream,self.base + offset,element_type)
+
+ @array_unloader(EnumConverter(uint32,gx.CullMode))
+ def unpack_cull_mode(array,material,entry):
+ material.cull_mode = array[entry.cull_mode_index]
+
+ @array_unloader(uint8)
+ def unpack_channel_count(array,material,entry):
+ material.channel_count = array[entry.channel_count_index]
+
+ @array_unloader(Color)
+ def unpack_material_color(array,material,entry):
+ for channel,index in zip(material.channels,entry.material_color_indices):
+ channel.material_color = array[index]
+
+ @array_unloader(Color)
+ def unpack_ambient_color(array,material,entry):
+ for channel,index in zip(material.channels,entry.ambient_color_indices):
+ channel.ambient_color = array[index]
+
+ @array_unloader(LightingMode)
+ def unpack_lighting_mode(array,material,entry):
+ for channel,channel_entry in zip(material.channels,entry.channels):
+ channel.color_mode = array[channel_entry.color_mode_index]
+ channel.alpha_mode = array[channel_entry.alpha_mode_index]
+
+ @array_unloader(Light)
+ def unpack_light(array,material,entry):
+ for i,index in enumerate(entry.light_indices):
+ if index is None: continue
+ material.lights[i] = array[index]
+
+ @array_unloader(uint8)
+ def unpack_texcoord_generator_count(array,material,entry):
+ material.texcoord_generator_count = array[entry.texcoord_generator_count_index]
+
+ @array_unloader(TexCoordGenerator)
+ def unpack_texcoord_generator(array,material,entry):
+ for i in range(material.texcoord_generator_count):
+ material.texcoord_generators[i] = array[entry.texcoord_generator_indices[i]]
+
+ @array_unloader(TextureMatrix)
+ def unpack_texture_matrix(array,material,entry):
+ for i,index in enumerate(entry.texture_matrix_indices):
+ if index is None: continue
+ material.texture_matrices[i] = array[index]
+
+ @array_unloader(uint16)
+ def unpack_texture_index(array,material,entry):
+ for i,index in enumerate(entry.texture_index_indices):
+ if index is None: continue
+ material.texture_indices[i] = array[index]
+
+ @array_unloader(uint8)
+ def unpack_tev_stage_count(array,material,entry):
+ material.tev_stage_count = array[entry.tev_stage_count_index]
+
+ @array_unloader(TevOrder)
+ def unpack_tev_order(array,material,entry):
+ for stage,index in zip(material.enabled_tev_stages,entry.tev_order_indices):
+ tev_order = array[index]
+ stage.texcoord = tev_order.texcoord
+ stage.texture = tev_order.texture
+ stage.color = tev_order.color
+
+ @array_unloader(TevCombiner)
+ def unpack_tev_combiner(array,material,entry):
+ for stage,index in zip(material.enabled_tev_stages,entry.tev_combiner_indices):
+ tev_combiner = array[index]
+ stage.unknown0 = tev_combiner.unknown0
+ stage.color_mode = tev_combiner.color_mode
+ stage.alpha_mode = tev_combiner.alpha_mode
+ stage.unknown1 = tev_combiner.unknown1
+
+ @array_unloader(SwapMode)
+ def unpack_swap_mode(array,material,entry):
+ for stage,index in zip(material.enabled_tev_stages,entry.swap_mode_indices):
+ swap_mode = array[index]
+ stage.color_swap_table = swap_mode.color_swap_table
+ stage.texture_swap_table = swap_mode.texture_swap_table
+
+ @array_unloader(ColorS16)
+ def unpack_tev_color(array,material,entry):
+ for i,index in enumerate(entry.tev_color_indices):
+ material.tev_colors[i] = array[index]
+
+ material.tev_color_previous = array[entry.tev_color_previous_index]
+
+ @array_unloader(Color)
+ def unpack_kcolor(array,material,entry):
+ for i,index in enumerate(entry.kcolor_indices):
+ material.kcolors[i] = array[index]
+
+ @array_unloader(SwapTable)
+ def unpack_swap_table(array,material,entry):
+ for i,index in enumerate(entry.swap_table_indices):
+ material.swap_tables[i] = array[index]
+
+ @array_unloader(Fog)
+ def unpack_fog(array,material,entry):
+ material.fog = array[entry.fog_index]
+
+ @array_unloader(AlphaTest)
+ def unpack_alpha_test(array,material,entry):
+ material.alpha_test = array[entry.alpha_test_index]
+
+ @array_unloader(BlendMode)
+ def unpack_blend_mode(array,material,entry):
+ material.blend_mode = array[entry.blend_mode_index]
+
+ @array_unloader(DepthMode)
+ def unpack_depth_mode(array,material,entry):
+ material.depth_mode = array[entry.depth_mode_index]
+
+ @array_unloader(bool8)
+ def unpack_depth_test_early(array,material,entry):
+ material.depth_test_early = array[entry.depth_test_early_index]
+
+ @array_unloader(bool8)
+ def unpack_dither(array,material,entry):
+ material.dither = array[entry.dither_index]
+
+ @array_unloader(UnknownStruct5)
+ def unpack_unknown5(array,material,entry):
+ material.unknown5 = array[entry.unknown5_index]
+
+
+class AmbientSourceSVR0:
+
+ @staticmethod
+ def pack(stream,value):
+ uint8.pack(stream,0xFF)
+
+ @staticmethod
+ def unpack(stream):
+ if uint8.unpack(stream) != 0xFF:
+ raise FormatError('invalid ambient source for SVR0')
+ return gx.SRC_REG
+
+ @staticmethod
+ def sizeof():
+ return uint8.sizeof()
+
+
+class ConstantColorSVR0:
+
+ @staticmethod
+ def pack(stream,value):
+ uint8.pack(stream,value if value is not None else 0xFF)
+
+ @staticmethod
+ def unpack(stream):
+ value = uint8.unpack(stream)
+ return gx.ConstantColor(value) if value != 0xFF else gx.TEV_KCSEL_1
+
+ @staticmethod
+ def sizeof():
+ return uint8.sizeof()
+
+
+class ConstantAlphaSVR0:
+
+ @staticmethod
+ def pack(stream,value):
+ uint8.pack(stream,value if value is not None else 0xFF)
+
+ @staticmethod
+ def unpack(stream):
+ value = uint8.unpack(stream)
+ return gx.ConstantAlpha(value) if value != 0xFF else gx.TEV_KASEL_1
+
+ @staticmethod
+ def sizeof():
+ return uint8.sizeof()
+
+
+class LightingModeSVR0(LightingMode,replace_fields=True):
+ ambient_source = AmbientSourceSVR0
+
+
+class EntrySVR0(Entry,replace_fields=True):
+ constant_colors = Array(ConstantColorSVR0,16)
+ constant_alphas = Array(ConstantAlphaSVR0,16)
+
+ def __init__(self):
+ super().__init__()
+ self.kcolor_indices = [0,1,2,3]
+ self.constant_colors = [None]*16
+ self.constant_alphas = [None]*16
+
+ @classmethod
+ def unpack(cls,stream):
+ entry = super().unpack(stream)
+
+ if entry.ambient_color_indices != [None]*2:
+ raise FormatError('invalid ambient color indices for SVR0')
+ if entry.light_indices != [None]*8:
+ raise FormatError('invalid light indices for SVR0')
+ if entry.texture_matrix_indices != [None]*10:
+ raise FormatError('invalid texture matrix indices for SVR0')
+ if entry.swap_mode_indices != [None]*16:
+ raise FormatError('invalid swap mode indices for SVR0')
+ if entry.tev_color_indices != [None]*3:
+ raise FormatError('invalid tev color indices for SVR0')
+ if entry.tev_color_previous_index is not None:
+ raise FormatError('invalid tev color previous index for SVR0')
+ if entry.kcolor_indices != [0,1,2,3]:
+ raise FormatError('invalid kcolor indices for SVR0')
+ if entry.swap_table_indices != [None]*4:
+ raise FormatError('invalid swap table indices for SVR0')
+ if entry.fog_index is not None:
+ raise FormatError('invalid fog index for SVR0')
+ if entry.dither_index is not None:
+ raise FormatError('invalid dither index for SVR0')
+ if entry.unknown5_index is not None:
+ raise FormatError('invalid unknown5 index for SVR0')
+
+ if entry.unknown3 != [0xFFFF]*20:
+ logger.warning('unknown3 different from default for SVR0')
+
+ return entry
+
+ def load_constant_colors(self,material):
+ for i,stage in enumerate(material.enabled_tev_stages):
+ self.constant_colors[i] = stage.constant_color
+
+ def load_constant_alphas(self,material):
+ for i,stage in enumerate(material.enabled_tev_stages):
+ self.constant_alphas[i] = stage.constant_alpha
+
+
+class SectionPackerSVR0(SectionPacker):
+
+ entry_type = EntrySVR0
+
+ def pack_indirect_entries(self,materials):
+ return 0
+
+ def pool_ambient_color(self,materials,entries):
+ return None
+
+ def pool_light(self,materials,entries):
+ return None
+
+ def pool_texture_matrix(self,materials,entries):
+ return None
+
+ @pool_loader(TevCombiner,equal_predicate=TevCombiner.__eq__)
+ def pool_tev_combiner(pool,material,entry):
+ for i,stage in enumerate(material.enabled_tev_stages):
+ entry.tev_combiner_indices[i] = pool[stage]
+
+ def pool_swap_mode(self,material,entries):
+ return None
+
+ def pool_tev_color(self,material,entries):
+ return None
+
+ def pool_swap_table(self,material,entries):
+ return None
+
+ def pool_fog(self,material,entries):
+ return None
+
+ def pool_dither(self,material,entries):
+ return None
+
+ def pool_unknown5(self,material,entries):
+ return None
+
+ @pool_loader(EnumConverter(uint32,gx.CullMode))
+ def pool_cull_mode(pool,material,entry):
+ entry.cull_mode_index = pool[material.cull_mode]
+
+ @pool_loader(Color)
+ def pool_material_color(pool,material,entry):
+ for i,channel in enumerate(material.enabled_channels):
+ entry.material_color_indices[i] = pool[channel.material_color]
+
+ @pool_loader(LightingModeSVR0)
+ def pool_lighting_mode(pool,material,entry):
+ for channel,channel_entry in zip(material.enabled_channels,entry.channels):
+ channel_entry.color_mode_index = pool[channel.color_mode]
+ channel_entry.alpha_mode_index = pool[channel.alpha_mode]
+
+ def pool_kcolor(self,materials,entries):
+ return Pool(Color,[Color(0xFF,0xFF,0xFF,0xFF)]*4)
+
+ @pool_loader(bool8)
+ def pool_depth_test_early(pool,material,entry):
+ entry.depth_test_early_index = pool[material.depth_test_early]
+
+
+class SectionUnpackerSVR0(SectionUnpacker):
+
+ entry_type = EntrySVR0
+
+ def unpack_indirect_entries(self,offset,materials):
+ if offset != 0:
+ raise FormatError('invalid indirect entry offset for SVR0')
+
+ def unpack_ambient_color(self,offset,materials,entries):
+ if offset != 0:
+ raise FormatError('invalid ambient color offset for SVR0')
+
+ def unpack_light(self,offset,materials,entries):
+ if offset != 0:
+ raise FormatError('invalid light offset for SVR0')
+
+ def unpack_texture_matrix(self,offset,materials,entries):
+ if offset != 0:
+ raise FormatError('invalid texture matrix offset for SVR0')
+ assert offset == 0
+
+ def unpack_swap_mode(self,offset,material,entries):
+ if offset != 0:
+ raise FormatError('invalid swap mode offset for SVR0')
+
+ def unpack_tev_color(self,offset,material,entries):
+ if offset != 0:
+ raise FormatError('invalid tev color offset for SVR0')
+
+ def unpack_swap_table(self,offset,material,entries):
+ if offset != 0:
+ raise FormatError('invalid swap table offset for SVR0')
+
+ def unpack_fog(self,offset,material,entries):
+ if offset != 0:
+ raise FormatError('invalid fog offset for SVR0')
+
+ def unpack_dither(self,offset,material,entries):
+ if offset != 0:
+ raise FormatError('invalid dither offset for SVR0')
+
+ def unpack_unknown5(self,offset,material,entries):
+ if offset != 0:
+ raise FormatError('invalid unknown5 offset for SVR0')
+
+ @array_unloader(Color)
+ def unpack_material_color(array,material,entry):
+ for channel,index in zip(material.enabled_channels,entry.material_color_indices):
+ channel.material_color = array[index]
+
+ @array_unloader(LightingModeSVR0)
+ def unpack_lighting_mode(array,material,entry):
+ for channel,channel_entry in zip(material.enabled_channels,entry.channels):
+ channel.color_mode = array[channel_entry.color_mode_index]
+ channel.alpha_mode = array[channel_entry.alpha_mode_index]
+
+ def unpack_kcolor(self,offset,materials,entries):
+ array = self.create_array(offset,Color)
+ for i in range(4):
+ if array[i] != Color(0xFF,0xFF,0xFF,0xFF):
+ raise FormatError('invalid kcolor for SVR0')
+
+
+def pack(stream,materials,subversion):
+ if subversion == b'SVR3':
+ packer = SectionPacker()
+ elif subversion == b'\xFF\xFF\xFF\xFF':
+ packer = SectionPackerSVR0()
+ else:
+ raise ValueError('invalid subversion')
+
+ packer.pack(stream,materials)
+
+
+def unpack(stream,subversion):
+ if subversion == b'SVR3':
+ unpacker = SectionUnpacker()
+ elif subversion == b'\xFF\xFF\xFF\xFF':
+ unpacker = SectionUnpackerSVR0()
+ else:
+ raise ValueError('invalid subversion')
+
+ return unpacker.unpack(stream)
+
diff --git a/j3d/material.py b/j3d/material.py
new file mode 100644
index 0000000..260ef49
--- /dev/null
+++ b/j3d/material.py
@@ -0,0 +1,591 @@
+from math import cos,sin,radians
+import copy
+import numpy
+from OpenGL.GL import *
+from btypes.big_endian import *
+import gl
+import gx
+from j3d.opengl import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Vector(Struct):
+ x = float32
+ y = float32
+ z = float32
+
+ def __init__(self,x=0,y=0,z=0):
+ self.x = x
+ self.y = y
+ self.z = z
+
+
+class Color(Struct):
+ r = uint8
+ g = uint8
+ b = uint8
+ a = uint8
+
+ def __init__(self,r=0x00,g=0x00,b=0x00,a=0xFF):
+ self.r = r
+ self.g = g
+ self.b = b
+ self.a = a
+
+ @staticmethod
+ def gl_type():
+ return gl.vec4
+
+ def gl_convert(self):
+ return numpy.array([self.r,self.g,self.b,self.a],numpy.float32)/0xFF
+
+
+class LightingMode(Struct):
+ """Arguments to GXSetChanCtrl."""
+ light_enable = bool8
+ material_source = EnumConverter(uint8,gx.ChannelSource)
+ light_mask = uint8
+ diffuse_function = EnumConverter(uint8,gx.DiffuseFunction)
+ attenuation_function = EnumConverter(uint8,gx.AttenuationFunction)
+ ambient_source = EnumConverter(uint8,gx.ChannelSource)
+ __padding__ = Padding(2)
+
+ def __init__(self):
+ self.light_enable = False
+ self.material_source = gx.SRC_REG
+ self.ambient_source = gx.SRC_REG
+ self.light_mask = gx.LIGHT_NULL
+ self.diffuse_function = gx.DF_NONE
+ self.attenuation_function = gx.AF_NONE
+
+
+class Channel:
+
+ def __init__(self):
+ self.color_mode = LightingMode()
+ self.alpha_mode = LightingMode()
+ self.material_color = Color(0xFF,0xFF,0xFF)
+ self.ambient_color = Color(0xFF,0xFF,0xFF)
+
+
+class Light(Struct):
+ position = Vector
+ direction = Vector
+ color = Color
+ a0 = float32
+ a1 = float32
+ a2 = float32
+ k0 = float32
+ k1 = float32
+ k2 = float32
+
+ def __init__(self):
+ self.position = Vector(0,0,0)
+ self.direction = Vector(0,0,0)
+ self.color = (0xFF,0xFF,0xFF)
+ self.a0 = 1
+ self.a1 = 0
+ self.a2 = 0
+ self.k0 = 1
+ self.k1 = 0
+ self.k2 = 0
+
+
+class TexCoordGenerator(Struct):
+ """Arguments to GXSetTexCoordGen."""
+ function = EnumConverter(uint8,gx.TexCoordFunction)
+ source = EnumConverter(uint8,gx.TexCoordSource)
+ matrix = EnumConverter(uint8,gx.TextureMatrix)
+ __padding__ = Padding(1)
+
+ def __init__(self):
+ self.function = gx.TG_MTX2x4
+ self.source = gx.TG_TEX0
+ self.matrix = gx.IDENTITY
+
+
+class TextureMatrix(Struct):
+ shape = EnumConverter(uint8,gx.TexCoordFunction)
+ matrix_type = uint8
+ __padding__ = Padding(2)
+ center_s = float32
+ center_t = float32
+ unknown0 = float32
+ scale_s = float32
+ scale_t = float32
+ rotation = FixedPointConverter(sint16,180/32768)
+ __padding__ = Padding(2)
+ translation_s = float32
+ translation_t = float32
+ projection_matrix = Array(Array(float32,4),4)
+
+ def __init__(self):
+ self.matrix_type = 0
+ self.shape = gx.TG_MTX2x4
+ self.center_s = 0.5
+ self.center_t = 0.5
+ self.unknown0 = 0.5
+ self.rotation = 0
+ self.scale_s = 1
+ self.scale_t = 1
+ self.translation_s = 0
+ self.translation_t = 0
+ self.projection_matrix = [[1,0,0,0],[0,1,0,0],[0,0,1,0],[0,0,0,1]]
+
+ def create_matrix(self):
+ c = cos(radians(self.rotation))
+ s = sin(radians(self.rotation))
+ R = numpy.matrix([[c,-s,0],[s,c,0],[0,0,1]])
+ S = numpy.matrix([[self.scale_s,0,0],[0,self.scale_t,0],[0,0,1]])
+ C = numpy.matrix([[1,0,self.center_s],[0,1,self.center_t],[0,0,1]])
+ T = numpy.matrix([[1,0,self.translation_s],[0,1,self.translation_t],[0,0,1]])
+
+ # Only types 0x00, 0x06, 0x07, 0x08 and 0x09 have been tested
+ if self.matrix_type in {0x00,0x02,0x0A,0x0B,0x80}:
+ P = numpy.matrix([[1,0,0,0],[0,1,0,0],[0,0,0,1]])
+ elif self.matrix_type == 0x06:
+ P = numpy.matrix([[0.5,0,0,0.5],[0,-0.5,0,0.5],[0,0,0,1]])
+ elif self.matrix_type == 0x07:
+ P = numpy.matrix([[0.5,0,0.5,0],[0,-0.5,0.5,0],[0,0,1,0]])
+ elif self.matrix_type in {0x08,0x09}:
+ P = numpy.matrix([[0.5,0,0.5,0],[0,-0.5,0.5,0],[0,0,1,0]])*numpy.matrix(self.projection_matrix)
+ else:
+ raise ValueError('invalid texture matrix type')
+
+ M = T*C*S*R*C.I*P
+
+ if self.shape == gx.TG_MTX2x4:
+ return M[:2,:]
+ elif self.shape == gx.TG_MTX3x4:
+ return M
+ else:
+ raise ValueError('invalid texture matrix shape')
+
+ def gl_type(self):
+ if self.shape == gx.TG_MTX2x4:
+ return gl.mat4x2
+ if self.shape == gx.TG_MTX3x4:
+ return gl.mat4x3
+
+ raise ValueError('invalid texture matrix shape')
+
+ def gl_convert(self):
+ return self.create_matrix()
+
+
+class TevColorMode(Struct):
+ """Arguments to GXSetTevColorIn and GXSetTevColorOp."""
+ a = EnumConverter(uint8,gx.ColorInput)
+ b = EnumConverter(uint8,gx.ColorInput)
+ c = EnumConverter(uint8,gx.ColorInput)
+ d = EnumConverter(uint8,gx.ColorInput)
+ function = EnumConverter(uint8,gx.TevFunction)
+ bias = EnumConverter(uint8,gx.TevBias)
+ scale = EnumConverter(uint8,gx.TevScale)
+ clamp = bool8
+ output = EnumConverter(uint8,gx.TevColor)
+
+ def __init__(self):
+ self.a = gx.CC_ZERO
+ self.b = gx.CC_ZERO
+ self.c = gx.CC_ZERO
+ self.d = gx.CC_TEXC
+ self.function = gx.TEV_ADD
+ self.bias = gx.TB_ZERO
+ self.scale = gx.CS_SCALE_1
+ self.clamp = True
+ self.output = gx.TEVPREV
+
+
+class TevAlphaMode(Struct):
+ """Arguments to GXSetTevAlphaIn and GXSetTevAlphaOp."""
+ a = EnumConverter(uint8,gx.AlphaInput)
+ b = EnumConverter(uint8,gx.AlphaInput)
+ c = EnumConverter(uint8,gx.AlphaInput)
+ d = EnumConverter(uint8,gx.AlphaInput)
+ function = EnumConverter(uint8,gx.TevFunction)
+ bias = EnumConverter(uint8,gx.TevBias)
+ scale = EnumConverter(uint8,gx.TevScale)
+ clamp = bool8
+ output = EnumConverter(uint8,gx.TevColor)
+
+ def __init__(self):
+ self.a = gx.CA_ZERO
+ self.b = gx.CA_ZERO
+ self.c = gx.CA_ZERO
+ self.d = gx.CA_TEXA
+ self.function = gx.TEV_ADD
+ self.bias = gx.TB_ZERO
+ self.scale = gx.CS_SCALE_1
+ self.clamp = True
+ self.output = gx.TEVPREV
+
+
+class TevStage:
+
+ def __init__(self):
+ self.unknown0 = 0xFF
+ self.unknown1 = 0xFF
+
+ self.texcoord = gx.TEXCOORD_NULL
+ self.texture = gx.TEXMAP_NULL
+ self.color = gx.COLOR_NULL
+
+ self.color_mode = TevColorMode()
+ self.alpha_mode = TevAlphaMode()
+
+ self.constant_color = gx.TEV_KCSEL_1
+ self.constant_alpha = gx.TEV_KASEL_1
+
+ self.color_swap_table = gx.TEV_SWAP0
+ self.texture_swap_table = gx.TEV_SWAP0
+
+ self.indirect_stage = gx.INDTEXSTAGE0
+ self.indirect_format = gx.ITF_8
+ self.indirect_bias_components = gx.ITB_NONE
+ self.indirect_matrix = gx.ITM_OFF
+ self.wrap_s = gx.ITW_OFF
+ self.wrap_t = gx.ITW_OFF
+ self.add_previous_texcoord = False
+ self.use_original_lod = False
+ self.bump_alpha = gx.ITBA_OFF
+
+
+class SwapTable(Struct):
+ """Arguments to GXSetTevSwapModeTable."""
+ r = EnumConverter(uint8,gx.ColorComponent)
+ g = EnumConverter(uint8,gx.ColorComponent)
+ b = EnumConverter(uint8,gx.ColorComponent)
+ a = EnumConverter(uint8,gx.ColorComponent)
+
+ def __init__(self):
+ self.r = gx.CH_RED
+ self.g = gx.CH_GREEN
+ self.b = gx.CH_BLUE
+ self.a = gx.CH_ALPHA
+
+
+class IndirectStage:
+
+ def __init__(self):
+ self.texcoord = gx.TEXCOORD_NULL
+ self.texture = gx.TEXMAP_NULL
+ self.scale_s = gx.ITS_1
+ self.scale_t = gx.ITS_1
+
+
+class IndirectMatrix(Struct):
+ """Arguments to GXSetIndTexMatrix."""
+ significand_matrix = Array(Array(float32,3),2)
+ scale_exponent = sint8
+ __padding__ = Padding(3)
+
+ def __init__(self):
+ self.significand_matrix = [[0.5,0,0],[0,0.5,0]]
+ self.scale_exponent = 1
+
+ @staticmethod
+ def gl_type():
+ return gl.mat3x2
+
+ def gl_convert(self):
+ matrix = numpy.zeros((2,4),numpy.float32) #FIXME
+ matrix[:,0:3] = numpy.array(self.significand_matrix,numpy.float32)*2**self.scale_exponent
+ return matrix
+
+
+class AlphaTest(Struct):
+ """Arguments to GXSetAlphaCompare."""
+ function0 = EnumConverter(uint8,gx.CompareFunction)
+ reference0 = uint8
+ operation = EnumConverter(uint8,gx.AlphaOperator)
+ function1 = EnumConverter(uint8,gx.CompareFunction)
+ reference1 = uint8
+ __padding__ = Padding(3)
+
+ def __init__(self):
+ self.function0 = gx.ALWAYS
+ self.reference0 = 0
+ self.function1 = gx.ALWAYS
+ self.reference1 = 0
+ self.operation = gx.AOP_AND
+
+
+class Fog(Struct):
+ """Arguments to GXSetFog and GXSetFogRangeAdj."""
+ function = EnumConverter(uint8,gx.FogFunction)
+ range_adjustment_enable = bool8
+ range_adjustment_center = uint16
+ z_start = float32
+ z_end = float32
+ z_near = float32
+ z_far = float32
+ color = Color
+ range_adjustment_table = Array(uint16,10)
+
+ def __init__(self):
+ self.function = gx.FOG_NONE
+ self.z_start = 0
+ self.z_end = 0
+ self.z_near = 0
+ self.z_far = 0
+ self.color = Color(0xFF,0xFF,0xFF)
+
+ self.range_adjustment_enable = False
+ self.range_adjustment_center = 0
+ self.range_adjustment_table = [0]*10
+
+
+class DepthMode(Struct):
+ """Arguments to GXSetZMode."""
+ enable = bool8
+ function = EnumConverter(uint8,gx.CompareFunction)
+ update_enable = bool8
+ __padding__ = Padding(1)
+
+ def __init__(self):
+ self.enable = True
+ self.function = gx.LEQUAL
+ self.update_enable = True
+
+
+class BlendMode(Struct):
+ """Arguments to GXSetBlendMode."""
+ function = EnumConverter(uint8,gx.BlendFunction)
+ source_factor = EnumConverter(uint8,gx.BlendSourceFactor)
+ destination_factor = EnumConverter(uint8,gx.BlendDestinationFactor)
+ logical_operation = EnumConverter(uint8,gx.LogicalOperation)
+
+ def __init__(self):
+ self.function = gx.BM_NONE
+ self.source_factor = gx.BL_SRCALPHA
+ self.destination_factor = gx.BL_INVSRCALPHA
+ self.logical_operation = gx.LO_CLEAR
+
+
+class UnknownStruct5(Struct):
+ unknown0 = uint8
+ __padding__ = Padding(3)
+ unknown1 = float32
+ unknown2 = float32
+ unknown3 = float32
+
+ def __init__(self):
+ self.unknown0 = 0
+ self.unknown1 = 1
+ self.unknown2 = 1
+ self.unknown3 = 1
+
+ @classmethod
+ def unpack(cls,stream):
+ unknown5 = super().unpack(stream)
+ if unknown5 != UnknownStruct5():
+ logger.warning('unknown5 different from default')
+ return unknown5
+
+
+class Material:
+
+ def __init__(self):
+ self.name = None
+ self.unknown0 = 1 # related to transparency sorting
+ self.cull_mode = gx.CULL_BACK
+
+ self.channel_count = 0
+ self.channels = [Channel() for _ in range(2)]
+ self.lights = [None]*8
+
+ self.texcoord_generator_count = 0
+ self.texcoord_generators = [TexCoordGenerator() for _ in range(8)]
+ self.texture_matrices = [None]*10
+ self.texture_indices = [None]*8
+
+ self.tev_stage_count = 0
+ self.tev_stages = [TevStage() for _ in range(16)]
+ self.tev_colors = [Color(0xFF,0xFF,0xFF) for _ in range(3)]
+ self.tev_color_previous = Color(0xFF,0xFF,0xFF)
+ self.kcolors = [Color(0xFF,0xFF,0xFF) for _ in range(4)]
+ self.swap_tables = [SwapTable() for _ in range(4)]
+
+ self.indirect_stage_count = 0
+ self.indirect_stages = [IndirectStage() for _ in range(4)]
+ self.indirect_matrices = [IndirectMatrix() for _ in range(3)]
+
+ self.alpha_test = AlphaTest()
+ self.fog = Fog()
+ self.depth_test_early = True
+ self.depth_mode = DepthMode()
+ self.blend_mode = BlendMode()
+ self.dither = True
+ self.unknown5 = UnknownStruct5()
+
+ self.unknown2 = [0xFFFF]*8
+ self.unknown3 = [0xFFFF]*20
+ self.unknown4 = [0xFFFF]*12
+
+ @property
+ def enabled_channels(self):
+ for i in range(self.channel_count):
+ yield self.channels[i]
+
+ @property
+ def enabled_texcoord_generators(self):
+ for i in range(self.texcoord_generator_count):
+ yield self.texcoord_generators[i]
+
+ @property
+ def enabled_tev_stages(self):
+ for i in range(self.tev_stage_count):
+ yield self.tev_stages[i]
+
+ @property
+ def enabled_indirect_stages(self):
+ for i in range(self.indirect_stage_count):
+ yield self.indirect_stages[i]
+
+ def update_use_variables(self):
+ self.use_normal = False
+ self.use_binormal = False
+ self.use_tangent = False
+ self.use_color = False
+ self.use_texcoord = [False]*8
+ self.use_material_color = [False]*2
+ self.use_ambient_color = [False]*2
+ self.use_texture_matrix = [False]*10
+ self.use_texture = [False]*8
+ self.use_indirect_matrix = [False]*3
+
+ for i,channel in enumerate(self.enabled_channels):
+ if channel.color_mode.material_source == gx.SRC_REG:
+ self.use_material_color[i] = True
+ elif channel.color_mode.material_source == gx.SRC_VTX:
+ self.use_color = True
+ else:
+ raise ValueError('invalid material source')
+
+ if channel.alpha_mode.material_source == gx.SRC_REG:
+ self.use_material_color[i] = True
+ elif channel.alpha_mode.material_source == gx.SRC_VTX:
+ self.use_color = True
+ else:
+ raise ValueError('inavlid material source')
+
+ if channel.color_mode.light_enable:
+ if channel.color_mode.ambient_source == gx.SRC_REG:
+ self.use_ambient_color[i] = True
+ elif channel.color_mode.ambient_source == gx.SRC_VTX:
+ self.use_color = True
+ else:
+ raise ValueError('invalid ambient source')
+
+ #if channel.alpha_mode.light_enable:
+ # if channel.alpha_mode.ambient_source == gx.SRC_REG:
+ # self.use_ambient_color[i] = True
+ # elif channel.alpha_mode.ambient_source == gx.SRC_VTX:
+ # self.use_color = True
+ # else:
+ # raise ValueError('invalid ambient source')
+
+ for generator in self.enabled_texcoord_generators:
+ if generator.function in {gx.TG_MTX2x4,gx.TG_MTX3x4}:
+ if generator.source == gx.TG_NRM:
+ self.use_normal = True
+ elif generator.source == gx.TG_BINRM:
+ self.use_binormal = True
+ elif generator.source == gx.TG_TANGENT:
+ self.use_tangent = True
+ elif generator.source in gx.TG_TEX:
+ self.use_texcoord[generator.source.index] = True
+
+ if generator.matrix != gx.IDENTITY:
+ self.use_texture_matrix[generator.matrix.index] = True
+
+ for stage in self.enabled_tev_stages:
+ if stage.texture != gx.TEXMAP_NULL:
+ self.use_texture[stage.texture.index] = True
+ if stage.indirect_matrix in gx.ITM:
+ self.use_indirect_matrix[stage.indirect_matrix.index] = True
+
+ for stage in self.enabled_indirect_stages:
+ self.use_texture[stage.texture.index] = True
+
+ def gl_init(self):
+ self.update_use_variables()
+
+ fields = []
+
+ fields.append(('tev_color0',self.tev_colors[0]))
+ fields.append(('tev_color1',self.tev_colors[1]))
+ fields.append(('tev_color2',self.tev_colors[2]))
+ fields.append(('tev_color_previous',self.tev_color_previous))
+ fields.append(('kcolor0',self.kcolors[0]))
+ fields.append(('kcolor1',self.kcolors[1]))
+ fields.append(('kcolor2',self.kcolors[2]))
+ fields.append(('kcolor3',self.kcolors[3]))
+
+ for i,channel in enumerate(self.enabled_channels):
+ if self.use_material_color[i]:
+ fields.append(('material_color{}'.format(i),channel.material_color))
+ if self.use_ambient_color[i]:
+ fields.append(('ambient_color{}'.format(i),channel.ambient_color))
+
+ for i,matrix in enumerate(self.texture_matrices):
+ if not self.use_texture_matrix[i]: continue
+ fields.append(('texture_matrix{}'.format(i),matrix))
+
+ for i,matrix in enumerate(self.indirect_matrices):
+ if not self.use_indirect_matrix[i]: continue
+ fields.append(('indmatrix{}'.format(i),matrix))
+
+ block_type = gl.uniform_block('MaterialBlock',((name,value.gl_type()) for name,value in fields))
+ self.gl_block = block_type(GL_DYNAMIC_DRAW)
+
+ for name,value in fields:
+ self.gl_block[name] = value.gl_convert()
+
+ self.gl_texture_indices = copy.copy(self.texture_indices)
+
+ def gl_bind(self,textures):
+ self.gl_block.bind(MATERIAL_BLOCK_BINDING_POINT)
+
+ for i,texture_index in enumerate(self.gl_texture_indices):
+ if texture_index is None: continue
+ textures[texture_index].gl_bind(TEXTURE_UNITS[i])
+
+ if self.cull_mode != gx.CULL_NONE:
+ glEnable(GL_CULL_FACE)
+ glCullFace(self.cull_mode.gl_value)
+ else:
+ glDisable(GL_CULL_FACE)
+
+ if self.depth_mode.enable:
+ glEnable(GL_DEPTH_TEST)
+ glDepthFunc(self.depth_mode.function.gl_value)
+ glDepthMask(self.depth_mode.update_enable)
+ else:
+ glDisable(GL_DEPTH_TEST)
+
+ if self.blend_mode.function == gx.BM_BLEND:
+ glEnable(GL_BLEND)
+ glBlendEquation(GL_FUNC_ADD)
+ glBlendFunc(self.blend_mode.source_factor.gl_value,self.blend_mode.destination_factor.gl_value)
+ elif self.blend_mode.function == gx.BM_SUBTRACT:
+ glEnable(GL_BLEND)
+ glBlendEquation(GL_FUNC_REVERSE_SUBTRACT)
+ glBlendFunc(GL_ONE,GL_ONE)
+ else:
+ glDisable(GL_BLEND)
+
+ if self.blend_mode.function == gx.BM_LOGIC:
+ glEnable(GL_COLOR_LOGIC_OP)
+ glLogicOp(self.blend_mode.logical_operation.gl_value)
+ else:
+ glDisable(GL_COLOR_LOGIC_OP)
+
+ if self.dither:
+ glEnable(GL_DITHER)
+ else:
+ glDisable(GL_DITHER)
+
diff --git a/j3d/mdl3.py b/j3d/mdl3.py
new file mode 100644
index 0000000..4babff4
--- /dev/null
+++ b/j3d/mdl3.py
@@ -0,0 +1,837 @@
+from math import log,floor,ceil,frexp
+from btypes.big_endian import *
+import gx
+import j3d.string_table
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ packet_count = uint16
+ __padding__ = Padding(2)
+ packet_offset = uint32
+ subpacket_location_offset = uint32
+ matrix_index_offset = uint32
+ unknown0_offset = uint32
+ index_offset = uint32
+ name_offset = uint32
+
+ def __init__(self):
+ self.magic = b'MDL3'
+
+
+class PacketLocation(Struct):
+ offset = uint32
+ size = uint32
+
+
+class SubpacketLocation(Struct):
+ channel_color_offset = uint16
+ channel_offset = uint16
+ texcoord_generator_offset = uint16
+ texture_offset = uint16
+ tev_offset = uint16
+ fog_offset = uint16
+ __padding__ = Padding(4)
+
+
+class BitField:
+
+ def __init__(self,value=0):
+ self.value = value
+
+ def __index__(self):
+ return self.value
+
+ def __int__(self):
+ return self.value
+
+ def __eq__(self,other):
+ return self.value == int(other)
+
+ def __setitem__(self,key,value):
+ if isinstance(key,slice):
+ mask = ~(-1 << (key.stop - key.start)) << key.start
+ self.value = (self.value & ~mask) | ((value << key.start) & mask)
+ else:
+ if value:
+ self.value |= 1 << key
+ else:
+ self.value &= ~(1 << key)
+
+
+class BPCommand(BitField):
+
+ def __init__(self,register,value=0):
+ super().__init__(register << 24 | value)
+
+ @property
+ def register(self):
+ return self.value >> 24
+
+ @staticmethod
+ def pack(stream,command):
+ uint8.pack(stream,0x61)
+ uint32.pack(stream,command.value)
+
+
+class BPMask(BPCommand):
+
+ def __init__(self,mask):
+ super().__init__(0xFE,mask)
+
+
+class XFCommand(list):
+
+ def __init__(self,register,values=tuple(),element_type=uint32):
+ super().__init__(values)
+ self.register = register
+ self.element_type = element_type
+
+ def __getitem__(self,key):
+ if isinstance(key,slice):
+ return XFCommand(self.register,super().__getitem__(key),self.element_type)
+ else:
+ return super().__getitem__(key)
+
+ @staticmethod
+ def pack(stream,command):
+ if not command: return
+ uint8.pack(stream,0x10)
+ uint16.pack(stream,len(command) - 1)
+ uint16.pack(stream,command.register)
+ for value in command:
+ command.element_type.pack(stream,value)
+
+
+def convert_cull_mode(mode):
+ if mode == gx.CULL_NONE:
+ return 0
+ if mode == gx.CULL_FRONT:
+ return 2
+ if mode == gx.CULL_BACK:
+ return 1
+ if mode == gx.CULL_ALL:
+ return 3
+
+ raise ValueError('invalid cull mode')
+
+
+def convert_rasterized_color(color):
+ if color in {gx.COLOR0,gx.ALPHA0,gx.COLOR0A0}:
+ return 0
+ if color in {gx.COLOR1,gx.ALPHA1,gx.COLOR1A1}:
+ return 1
+ if color == gx.COLOR_ZERO:
+ return 7
+ if color == gx.ALPHA_BUMP:
+ return 5
+ if color == gx.ALPHA_BUMPN:
+ return 6
+ if color == gx.COLOR_NULL:
+ return 7
+
+ raise ValueError('invalid rasterized color')
+
+
+def convert_texcoord_source(source):
+ if source == gx.TG_POS:
+ return 0
+ if source == gx.TG_NRM:
+ return 1
+ if source == gx.TG_BINRM:
+ return 3
+ if source == gx.TG_TANGENT:
+ return 4
+ if source in gx.TG_TEX:
+ return source + 1
+ if source in gx.TG_TEXCOORD:
+ return 5
+ if source in gx.TG_COLOR:
+ return 2
+
+ raise ValueError('invalid texture coordinate generator source')
+
+
+def convert_minification_filter(mode):
+ if mode == gx.NEAR:
+ return 0
+ if mode == gx.LINEAR:
+ return 4
+ if mode == gx.NEAR_MIP_NEAR:
+ return 1
+ if mode == gx.LIN_MIP_NEAR:
+ return 5
+ if mode == gx.NEAR_MIP_LIN:
+ return 2
+ if mode == gx.LIN_MIP_LIN:
+ return 6
+
+ raise ValueError('invalid minification filter')
+
+
+class Packet:
+
+ def __init__(self,material,textures):
+ self.init_commands()
+
+ self.use_texture = [False]*8
+ self.use_texture_matrix = [False]*10
+
+ for generator in material.enabled_texcoord_generators:
+ if generator.matrix != gx.IDENTITY:
+ self.use_texture_matrix[generator.matrix.index] = True
+
+ for stage in material.enabled_tev_stages:
+ if stage.texture != gx.TEXMAP_NULL:
+ self.SetTexCoordScale(stage.texcoord.index,textures[material.texture_indices[stage.texture.index]])
+ self.use_texture[stage.texture.index] = True
+
+ for stage in material.enabled_indirect_stages:
+ self.SetTexCoordScale(stage.texcoord.index,textures[material.texture_indices[stage.texture.index]])
+ self.SetTextureIndirect(stage.texture.index)
+ self.use_texture[stage.texture.index] = True
+
+ self.SetCullMode(material.cull_mode)
+
+ self.SetNumChans(material.channel_count)
+
+ for i,channel in enumerate(material.channels):
+ self.SetChanMatColor(i,channel.material_color)
+ self.SetChanAmbColor(i,channel.ambient_color)
+ self.SetChanCtrl(gx.COLOR[i],channel.color_mode)
+ self.SetChanCtrl(gx.ALPHA[i],channel.alpha_mode)
+
+ for i,light in enumerate(material.lights):
+ if light is None: continue
+ self.SetLight(i,light)
+
+ self.SetNumTexGens(material.texcoord_generator_count)
+
+ for i,generator in enumerate(material.texcoord_generators):
+ self.SetTexCoordGen(i,generator)
+
+ for i,matrix in enumerate(material.texture_matrices):
+ if not self.use_texture_matrix[i]: continue
+ self.SetTexMatrix(i,matrix)
+
+ for i,texture_index in enumerate(material.texture_indices):
+ if not self.use_texture[i]: continue
+ self.SetTexture(i,texture_index,textures[texture_index])
+
+ self.SetNumTevStages(material.tev_stage_count)
+
+ for i,stage in enumerate(material.tev_stages):
+ self.SetTevOrder(i,stage)
+ self.SetTevColorIn(i,stage.color_mode)
+ self.SetTevColorOp(i,stage.color_mode)
+ self.SetTevAlphaIn(i,stage.alpha_mode)
+ self.SetTevAlphaOp(i,stage.alpha_mode)
+ self.SetTevKColorSel(i,stage.constant_color)
+ self.SetTevKAlphaSel(i,stage.constant_alpha)
+ self.SetTevSwapMode(i,stage)
+ self.SetTevIndirect(i,stage)
+
+ for i,color in enumerate(material.tev_colors):
+ self.SetTevColor(gx.TEVREG[i],color)
+
+ self.SetTevColor(gx.TEVPREV,material.tev_color_previous)
+
+ for i,color in enumerate(material.kcolors):
+ self.SetTevKColor(i,color)
+
+ for i,table in enumerate(material.swap_tables):
+ self.SetTevSwapModeTable(i,table)
+
+ self.SetNumIndStages(material.indirect_stage_count)
+
+ for i,stage in enumerate(material.indirect_stages):
+ self.SetIndTexOrder(i,stage)
+ self.SetIndTexCoordScale(i,stage)
+
+ for i,matrix in enumerate(material.indirect_matrices):
+ self.SetIndTexMatrix(i,matrix)
+
+ self.SetAlphaCompare(material.alpha_test)
+ self.SetFog(material.fog)
+ self.SetZCompLoc(material.depth_test_early)
+ self.SetZMode(material.depth_mode)
+ self.SetFogRangeAdj(material.fog)
+ self.SetBlendMode(material.blend_mode)
+ self.SetDither(material.dither)
+
+ def init_commands(self):
+ self.tx_setmode0 = [BPCommand(reg) for reg in (0x80,0x81,0x82,0x83,0xA0,0xA1,0xA2,0xA3)]
+ self.tx_setmode1 = [BPCommand(reg) for reg in (0x84,0x85,0x86,0x87,0xA4,0xA5,0xA6,0xA7)]
+ self.tx_setimage0 = [BPCommand(reg) for reg in (0x88,0x89,0x8A,0x8B,0xA8,0xA9,0xAA,0xAB)]
+ self.tx_setimage3 = [BPCommand(reg) for reg in (0x94,0x95,0x96,0x97,0xB4,0xB5,0xB6,0xB7)]
+ self.loadtlut0 = [BPCommand(0x64) for _ in range(8)]
+ self.loadtlut1 = [BPCommand(0x65) for _ in range(8)]
+ self.tx_settlut = [BPCommand(reg) for reg in (0x98,0x99,0x9A,0x9B,0xB8,0xB9,0xBA,0xBB)]
+ self.tref = [BPCommand(0x28 + i) for i in range(8)]
+ self.su_ssize = [BPCommand(0x30 + 2*i) for i in range(8)]
+ self.su_tsize = [BPCommand(0x31 + 2*i) for i in range(8)]
+
+ self.tev_color_ra = [BPCommand(0xE0 + 2*i) for i in range(4)]
+ self.tev_color_bg = [BPCommand(0xE1 + 2*i) for i in range(4)]
+ self.kcolor_ra = [BPCommand(0xE0 + 2*i,1 << 23) for i in range(4)]
+ self.kcolor_bg = [BPCommand(0xE1 + 2*i,1 << 23) for i in range(4)]
+ self.tev_color_env = [BPCommand(0xC0 + 2*i) for i in range(16)]
+ self.tev_alpha_env = [BPCommand(0xC1 + 2*i) for i in range(16)]
+ self.ind_cmd = [BPCommand(0x10 + i) for i in range(16)]
+ self.tev_ksel = [BPCommand(0xF6 + i) for i in range(8)]
+
+ self.ind_mtxa = [BPCommand(0x06 + 3*i) for i in range(3)]
+ self.ind_mtxb = [BPCommand(0x07 + 3*i) for i in range(3)]
+ self.ind_mtxc = [BPCommand(0x08 + 3*i) for i in range(3)]
+ self.ras1_ss = [BPCommand(0x25 + i) for i in range(2)]
+ self.iref = BPCommand(0x27,0xFFFFFF)
+ self.ind_imask = BPCommand(0x0F)
+
+ self.fog_param0 = BPCommand(0xEE)
+ self.fog_param1 = BPCommand(0xEF)
+ self.fog_param2 = BPCommand(0xF0)
+ self.fog_param3 = BPCommand(0xF1)
+ self.fog_color = BPCommand(0xF2)
+ self.fog_table = [BPCommand(0xE9 + i) for i in range(5)]
+ self.fog_range = BPCommand(0xE8)
+
+ self.alphacompare = BPCommand(0xF3)
+ self.blendmode = BPCommand(0x41)
+ self.zmode = BPCommand(0x40)
+ self.zcompare = BPCommand(0x43)
+ self.genmode = BPCommand(0x00)
+
+ self.texmtx = [XFCommand(0x0078 + 12*i,element_type=float32) for i in range(10)]
+ self.texcoordgen0 = XFCommand(0x1040,[BitField() for _ in range(8)])
+ self.texcoordgen1 = XFCommand(0x1050,[BitField() for _ in range(8)])
+ self.matcolor = XFCommand(0x100C,[BitField() for _ in range(2)])
+ self.ambcolor = XFCommand(0x100A,[BitField() for _ in range(2)])
+ self.chanctrl = XFCommand(0x100E,[BitField() for _ in range(4)])
+
+ self.light_color = [XFCommand(0x0603 + 13*i,[BitField()]) for i in range(8)]
+ self.light_attn = [XFCommand(0x0604 + 13*i,[0,0,0,0,0,0],element_type=float32) for i in range(8)]
+ self.light_pos = [XFCommand(0x060A + 13*i,[0,0,0],element_type=float32) for i in range(8)]
+ self.light_dir = [XFCommand(0x060D + 13*i,[0,0,0],element_type=float32) for i in range(8)]
+
+ self.numchans = XFCommand(0x1009,[0])
+ self.numtexgens = XFCommand(0x103F,[0])
+
+ self.mtxidx = [BitField(0x3CF3CF00),BitField(0x00F3CF3C)] # CP 0x30,0x40
+
+ def SetNumChans(self,count):
+ self.genmode[4:7] = count
+ self.numchans[0] = count
+
+ def SetChanCtrl(self,i,mode):
+ self.chanctrl[i][0:1] = mode.material_source
+ self.chanctrl[i][1] = mode.light_enable
+ self.chanctrl[i][2:6] = mode.light_mask
+ self.chanctrl[i][6:7] = mode.ambient_source
+ self.chanctrl[i][7:9] = mode.diffuse_function if mode.attenuation_function != gx.AF_SPEC else gx.DF_NONE
+ self.chanctrl[i][9] = mode.attenuation_function != gx.AF_NONE
+ self.chanctrl[i][10] = mode.attenuation_function != gx.AF_SPEC
+ self.chanctrl[i][11:15] = mode.light_mask >> 4
+
+ def SetChanMatColor(self,i,color):
+ self.matcolor[i][0:8] = color.a
+ self.matcolor[i][8:16] = color.b
+ self.matcolor[i][16:24] = color.g
+ self.matcolor[i][24:32] = color.r
+
+ def SetChanAmbColor(self,i,color):
+ self.ambcolor[i][0:8] = color.a
+ self.ambcolor[i][8:16] = color.b
+ self.ambcolor[i][16:24] = color.g
+ self.ambcolor[i][24:32] = color.r
+
+ def SetLight(self,i,light):
+ self.light_color[i][0][0:8] = light.color.a
+ self.light_color[i][0][8:16] = light.color.b
+ self.light_color[i][0][16:24] = light.color.g
+ self.light_color[i][0][24:32] = light.color.r
+ self.light_attn[i][0] = light.a0
+ self.light_attn[i][1] = light.a1
+ self.light_attn[i][2] = light.a2
+ self.light_attn[i][3] = light.k0
+ self.light_attn[i][4] = light.k1
+ self.light_attn[i][5] = light.k2
+ self.light_pos[i][0] = light.position.x
+ self.light_pos[i][1] = light.position.y
+ self.light_pos[i][2] = light.position.z
+ self.light_dir[i][0] = light.direction.x
+ self.light_dir[i][1] = light.direction.y
+ self.light_dir[i][2] = light.direction.z
+
+ def SetNumTexGens(self,count):
+ self.genmode[0:4] = count
+ self.numtexgens[0] = count
+
+ def SetTexCoordGen(self,i,generator):
+ self.texcoordgen0[i][2:3] = generator.source in {gx.TG_POS,gx.TG_NRM,gx.TG_BINRM,gx.TG_TANGENT}
+ self.texcoordgen0[i][7:12] = convert_texcoord_source(generator.source)
+ self.texcoordgen0[i][12:15] = 5
+
+ if generator.function == gx.TG_MTX3x4:
+ self.texcoordgen0[i][1:2] = 1
+ elif generator.function in gx.TG_BUMP:
+ self.texcoordgen0[i][4:7] = 1
+ self.texcoordgen0[i][12:15] = generator.source.index
+ self.texcoordgen0[i][15:18] = generator.function.index
+ elif generator.function == gx.TG_SRTG:
+ self.texcoordgen0[i][4:7] = generator.source.index + 2
+
+ self.texcoordgen1[i][8] = False
+ self.texcoordgen1[i][0:6] = gx.PTTIDENTITY - gx.PTTMTX0
+
+ index,offset = divmod(6 + 6*i,30)
+ self.mtxidx[index][offset:offset + 6] = generator.matrix
+
+ def SetTexCoordScale(self,i,texture):
+ self.su_ssize[i][0:16] = texture.width - 1
+ self.su_tsize[i][0:16] = texture.height - 1
+
+ def SetTexMatrix(self,i,matrix):
+ self.texmtx[i][:] = matrix.create_matrix().flat
+
+ def SetTexture(self,i,texture_index,texture):
+ self.tx_setimage0[i][0:10] = texture.width - 1
+ self.tx_setimage0[i][10:20] = texture.height - 1
+ self.tx_setimage0[i][20:24] = texture.image_format
+ self.tx_setimage3[i][0:24] = texture_index
+ self.tx_setmode0[i][0:2] = texture.wrap_s
+ self.tx_setmode0[i][2:4] = texture.wrap_t
+ self.tx_setmode0[i][4] = texture.magnification_filter == gx.LINEAR
+ self.tx_setmode0[i][5:8] = convert_minification_filter(texture.minification_filter)
+ self.tx_setmode0[i][8] = True
+ self.tx_setmode0[i][9:17] = int(32*texture.lod_bias)
+ self.tx_setmode0[i][19:21] = gx.ANISO_1
+ self.tx_setmode0[i][21] = False
+ self.tx_setmode1[i][0:8] = int(16*texture.minimum_lod)
+ self.tx_setmode1[i][8:16] = int(16*texture.maximum_lod)
+
+ if texture.image_format in {gx.TF_CI4,gx.TF_CI8,gx.TF_CI14}:
+ #XXX BP 0x64 (loadtlut0) is used to specify the address of the
+ # palette in main memory, but there is no way to know that address
+ # until the model is loaded.
+ self.loadtlut0[i][0:24] = 0
+ self.loadtlut1[i][0:10] = 0x380 + 16*i
+ self.loadtlut1[i][10:20] = 1 if len(texture.palette) == 16 else 16
+ self.tx_settlut[i][0:10] = 0x380 + 16*i
+ self.tx_settlut[i][10:12] = texture.palette.palette_format
+
+ def SetTextureIndirect(self,i):
+ self.ind_imask[i] = True
+
+ def SetNumTevStages(self,count):
+ self.genmode[10:14] = count - 1
+
+ def SetTevOrder(self,i,stage):
+ index,offset = divmod(12*i,24)
+ self.tref[index][offset:offset + 3] = stage.texture
+ self.tref[index][offset + 3:offset + 6] = stage.texcoord if stage.texture != gx.TEXMAP_NULL else 0
+ self.tref[index][offset + 6] = stage.texture != gx.TEXMAP_NULL
+ self.tref[index][offset + 7:offset + 10] = convert_rasterized_color(stage.color)
+
+ def SetTevColorIn(self,i,mode):
+ self.tev_color_env[i][0:4] = mode.d
+ self.tev_color_env[i][4:8] = mode.c
+ self.tev_color_env[i][8:12] = mode.b
+ self.tev_color_env[i][12:16] = mode.a
+
+ def SetTevAlphaIn(self,i,mode):
+ self.tev_alpha_env[i][4:7] = mode.d
+ self.tev_alpha_env[i][7:10] = mode.c
+ self.tev_alpha_env[i][10:13] = mode.b
+ self.tev_alpha_env[i][13:16] = mode.a
+
+ def SetTevColorOp(self,i,mode):
+ self.tev_color_env[i][18:19] = mode.function
+ self.tev_color_env[i][19] = mode.clamp
+ self.tev_color_env[i][22:24] = mode.output
+
+ if mode.function in {gx.TEV_ADD,gx.TEV_SUB}:
+ self.tev_color_env[i][16:18] = mode.bias
+ self.tev_color_env[i][20:22] = mode.scale
+ else:
+ self.tev_color_env[i][16:18] = 3
+ self.tev_color_env[i][20:22] = mode.function >> 1
+
+ def SetTevAlphaOp(self,i,mode):
+ self.tev_alpha_env[i][18:19] = mode.function
+ self.tev_alpha_env[i][19] = mode.clamp
+ self.tev_alpha_env[i][22:24] = mode.output
+
+ if mode.function in {gx.TEV_ADD,gx.TEV_SUB}:
+ self.tev_alpha_env[i][16:18] = mode.bias
+ self.tev_alpha_env[i][20:22] = mode.scale
+ else:
+ self.tev_alpha_env[i][16:18] = 3
+ self.tev_alpha_env[i][20:22] = mode.function >> 1
+
+ def SetTevKColorSel(self,i,constant_color):
+ index,offset = divmod(4 + 10*i,20)
+ self.tev_ksel[index][offset:offset + 5] = constant_color
+
+ def SetTevKAlphaSel(self,i,constant_alpha):
+ index,offset = divmod(9 + 10*i,20)
+ self.tev_ksel[index][offset:offset + 5] = constant_alpha
+
+ def SetTevSwapMode(self,i,stage):
+ self.tev_alpha_env[i][0:2] = stage.color_swap_table
+ self.tev_alpha_env[i][2:4] = stage.texture_swap_table
+
+ def SetTevIndirect(self,i,stage):
+ self.ind_cmd[i][0:2] = stage.indirect_stage
+ self.ind_cmd[i][2:4] = stage.indirect_format
+ self.ind_cmd[i][4:7] = stage.indirect_bias_components
+ self.ind_cmd[i][7:9] = stage.bump_alpha
+ self.ind_cmd[i][9:13] = stage.indirect_matrix
+ self.ind_cmd[i][13:16] = stage.wrap_s
+ self.ind_cmd[i][16:19] = stage.wrap_t
+ self.ind_cmd[i][19] = stage.use_original_lod
+ self.ind_cmd[i][20] = stage.add_previous_texcoord
+
+ def SetTevColor(self,i,color):
+ self.tev_color_ra[i][0:11] = color.r
+ self.tev_color_ra[i][12:23] = color.a
+ self.tev_color_bg[i][0:11] = color.b
+ self.tev_color_bg[i][12:23] = color.g
+
+ def SetTevKColor(self,i,color):
+ self.kcolor_ra[i][0:11] = color.r
+ self.kcolor_ra[i][12:23] = color.a
+ self.kcolor_bg[i][0:11] = color.b
+ self.kcolor_bg[i][12:23] = color.g
+
+ def SetTevSwapModeTable(self,i,table):
+ self.tev_ksel[2*i][0:2] = table.r
+ self.tev_ksel[2*i][2:4] = table.g
+ self.tev_ksel[2*i + 1][0:2] = table.b
+ self.tev_ksel[2*i + 1][2:4] = table.a
+
+ def SetNumIndStages(self,count):
+ self.genmode[16:19] = count
+
+ def SetIndTexCoordScale(self,i,stage):
+ index,offset = divmod(8*i,24)
+ self.ras1_ss[index][offset:offset + 4] = stage.scale_s
+ self.ras1_ss[index][offset + 4:offset + 8] = stage.scale_t
+
+ def SetIndTexOrder(self,i,stage):
+ self.iref[6*i:6*i + 3] = stage.texture
+ self.iref[6*i + 3:6*i + 6] = stage.texcoord
+
+ def SetIndTexMatrix(self,i,matrix):
+ s = matrix.scale_exponent + 17
+ self.ind_mtxa[i][0:11] = int(1024*matrix.significand_matrix[0][0])
+ self.ind_mtxa[i][11:22] = int(1024*matrix.significand_matrix[1][0])
+ self.ind_mtxa[i][22:24] = s
+ self.ind_mtxb[i][0:11] = int(1024*matrix.significand_matrix[0][1])
+ self.ind_mtxb[i][11:22] = int(1024*matrix.significand_matrix[1][1])
+ self.ind_mtxb[i][22:24] = s >> 2
+ self.ind_mtxc[i][0:11] = int(1024*matrix.significand_matrix[0][2])
+ self.ind_mtxc[i][11:22] = int(1024*matrix.significand_matrix[1][2])
+ self.ind_mtxc[i][22:24] = s >> 4
+
+ def SetCullMode(self,mode):
+ self.genmode[14:16] = convert_cull_mode(mode)
+
+ def SetFog(self,fog):
+ projection = (fog.function >> 3) & 1
+
+ if projection:
+ if fog.z_far == fog.z_near or fog.z_end == fog.z_start:
+ A = 0
+ C = 0
+ else:
+ A = (fog.z_far - fog.z_near)/(fog.z_end - fog.z_start)
+ C = (fog.z_start - fog.z_near)/(fog.z_end - fog.z_start)
+ b_shift = 0
+ b_magnitude = 0
+
+ else:
+ if fog.z_far == fog.z_near or fog.z_end == fog.z_start:
+ A = 0
+ B = 0.5
+ C = 0
+ else:
+ A = fog.z_far*fog.z_near/((fog.z_far - fog.z_near)*(fog.z_end - fog.z_start))
+ B = fog.z_far/(fog.z_far - fog.z_near)
+ C = fog.z_start/(fog.z_end - fog.z_start)
+
+ if B > 1:
+ b_shift = 1 + int(ceil(log(B,2)))
+ elif 0 < B < 0.5:
+ b_shift = 0
+ else:
+ b_shift = 1
+
+ A /= 2**b_shift
+ b_magnitude = int(2*(B/2**b_shift)*8388638)
+
+
+ a_mantissa,a_exponent = frexp(A)
+ self.fog_param0[0:11] = int(abs(a_mantissa)*2**12) & 0x7FF
+ self.fog_param0[11:19] = a_exponent + 126 if A != 0 else 0
+ self.fog_param0[19] = a_mantissa < 0
+
+ self.fog_param1[0:24] = b_magnitude
+ self.fog_param2[0:5] = b_shift
+
+ c_mantissa,c_exponent = frexp(C)
+ self.fog_param3[0:11] = int(abs(c_mantissa)*2**12) & 0x7FF
+ self.fog_param3[11:19] = c_exponent + 126 if C != 0 else 0
+ self.fog_param3[19] = c_mantissa < 0
+ self.fog_param3[20:21] = projection
+ self.fog_param3[21:24] = fog.function
+
+ self.fog_color[0:8] = fog.color.b
+ self.fog_color[8:16] = fog.color.g
+ self.fog_color[16:24] = fog.color.r
+
+ def SetFogRangeAdj(self,fog):
+ self.fog_range[0:10] = fog.range_adjustment_center + 342
+ self.fog_range[10] = fog.range_adjustment_enable
+
+ if fog.range_adjustment_enable:
+ for i in range(10):
+ index,offset = divmod(12*i,24)
+ self.fog_table[index][offset:offset + 12] = fog.range_adjustment_table[i]
+
+ def SetAlphaCompare(self,mode):
+ self.alphacompare[0:8] = mode.reference0
+ self.alphacompare[8:16] = mode.reference1
+ self.alphacompare[16:19] = mode.function0
+ self.alphacompare[19:22] = mode.function1
+ self.alphacompare[22:24] = mode.operation
+
+ def SetBlendMode(self,mode):
+ self.blendmode[0] = mode.function in {gx.BM_BLEND,gx.BM_SUBTRACT}
+ self.blendmode[1] = mode.function == gx.BM_LOGIC
+ self.blendmode[5:8] = mode.destination_factor
+ self.blendmode[11] = mode.function == gx.BM_SUBTRACT
+ self.blendmode[8:11] = mode.source_factor
+ self.blendmode[12:16] = mode.logical_operation
+
+ def SetZCompLoc(self,depth_test_early):
+ self.zcompare[6] = depth_test_early
+
+ def SetZMode(self,mode):
+ self.zmode[0] = mode.enable
+ self.zmode[1:4] = mode.function
+ self.zmode[4] = mode.update_enable
+
+ def SetDither(self,dither):
+ self.blendmode[2] = dither
+
+
+def pack_texture_subpacket(stream,packet,material,textures):
+ for i in range(8):
+ if not packet.use_texture[i]: continue
+
+ BPCommand.pack(stream,packet.tx_setimage3[i])
+ BPCommand.pack(stream,packet.tx_setimage0[i])
+ BPCommand.pack(stream,packet.tx_setmode0[i])
+ BPCommand.pack(stream,packet.tx_setmode1[i])
+
+ if textures[material.texture_indices[i]].image_format in {gx.TF_CI4,gx.TF_CI8,gx.TF_CI14}:
+ BPCommand.pack(stream,BPMask(0xFFFF00))
+ BPCommand.pack(stream,BPCommand(0x0F,0))
+ BPCommand.pack(stream,packet.loadtlut0[i])
+ BPCommand.pack(stream,packet.loadtlut1[i])
+ BPCommand.pack(stream,BPMask(0xFFFF00))
+ BPCommand.pack(stream,BPCommand(0x0F,0))
+ BPCommand.pack(stream,packet.tx_settlut[i])
+
+ for i in range((material.tev_stage_count + 1)//2):
+ BPCommand.pack(stream,packet.tref[i])
+
+ for j in range(2):
+ stage = material.tev_stages[2*i + j]
+ use_texture = stage.texture != gx.TEXMAP_NULL
+ texcoord = stage.texcoord if use_texture else gx.TEXCOORD7
+ BPCommand.pack(stream,BPMask(0x03FFFF))
+ BPCommand.pack(stream,packet.su_ssize[texcoord.index])
+ BPCommand.pack(stream,packet.su_tsize[texcoord.index])
+
+
+def pack_tev_subpacket(stream,packet,material):
+ # Notice that GX_TEVPREV is not set
+ for i in gx.TEVREG:
+ BPCommand.pack(stream,packet.tev_color_ra[i])
+ BPCommand.pack(stream,packet.tev_color_bg[i])
+ BPCommand.pack(stream,packet.tev_color_bg[i])
+ BPCommand.pack(stream,packet.tev_color_bg[i])
+
+ for i in range(4):
+ BPCommand.pack(stream,packet.kcolor_ra[i])
+ BPCommand.pack(stream,packet.kcolor_bg[i])
+
+ for i in range(material.tev_stage_count):
+ BPCommand.pack(stream,packet.tev_color_env[i])
+ BPCommand.pack(stream,packet.tev_alpha_env[i])
+ BPCommand.pack(stream,packet.ind_cmd[i])
+
+ for i in range(8):
+ BPCommand.pack(stream,packet.tev_ksel[i])
+
+ # This is what Nintendo does, though I would say that this is wrong. The
+ # number of enabled indirect texture stages does not in general have anything
+ # to do with which indirect texture matrices that are used.
+ for i in range(material.indirect_stage_count):
+ BPCommand.pack(stream,packet.ind_mtxa[i])
+ BPCommand.pack(stream,packet.ind_mtxb[i])
+ BPCommand.pack(stream,packet.ind_mtxc[i])
+
+ for i in range((material.indirect_stage_count + 1)//2):
+ BPCommand.pack(stream,packet.ras1_ss[i])
+
+ for i,stage in enumerate(material.enabled_indirect_stages):
+ BPCommand.pack(stream,BPMask(0x03FFFF))
+ BPCommand.pack(stream,packet.su_ssize[stage.texcoord.index])
+ BPCommand.pack(stream,packet.su_tsize[stage.texcoord.index])
+
+ # These commands for the disabled indirect stages might seem a bit odd, but
+ # notice that 0x30 + 2*(-1) = 0x2E and 0x31 + 2*(-1) = 0x2F
+ for i in range(4 - material.indirect_stage_count):
+ BPCommand.pack(stream,BPMask(0x03FFFF))
+ BPCommand.pack(stream,BPCommand(0x2E))
+ BPCommand.pack(stream,BPCommand(0x2F))
+
+ BPCommand.pack(stream,packet.iref)
+ BPCommand.pack(stream,packet.ind_imask)
+
+
+def pack_fog_subpacket(stream,packet,material):
+ BPCommand.pack(stream,packet.fog_param0)
+ BPCommand.pack(stream,packet.fog_param1)
+ BPCommand.pack(stream,packet.fog_param2)
+ BPCommand.pack(stream,packet.fog_param3)
+ BPCommand.pack(stream,packet.fog_color)
+
+ if material.fog.range_adjustment_enable:
+ for i in range(5):
+ BPCommand.pack(stream,packet.fog_table[i])
+
+ BPCommand.pack(stream,packet.fog_range)
+
+ BPCommand.pack(stream,packet.alphacompare)
+ # In Wind Waker this mask is 0x001FE7
+ BPCommand.pack(stream,BPMask(0x00FFE7))
+ BPCommand.pack(stream,packet.blendmode)
+ BPCommand.pack(stream,packet.zmode)
+ BPCommand.pack(stream,BPMask(0x000040))
+ BPCommand.pack(stream,packet.zcompare)
+ BPCommand.pack(stream,BPMask(0x07FC3F))
+ BPCommand.pack(stream,packet.genmode)
+
+
+def pack_texcoord_generator_subpacket(stream,packet,material):
+ for i in range(10):
+ if not packet.use_texture_matrix[i]: continue
+ XFCommand.pack(stream,packet.texmtx[i])
+
+ XFCommand.pack(stream,packet.texcoordgen0[0:material.texcoord_generator_count])
+ XFCommand.pack(stream,packet.texcoordgen1[0:material.texcoord_generator_count])
+
+
+def pack_channel_color_subpacket(stream,packet):
+ XFCommand.pack(stream,packet.matcolor)
+ XFCommand.pack(stream,packet.ambcolor)
+
+
+def pack_channel_subpacket(stream,packet,material):
+ XFCommand.pack(stream,packet.chanctrl)
+
+ for i in range(8):
+ if material.lights[i] is None: continue
+ XFCommand.pack(stream,packet.light_pos[i])
+ XFCommand.pack(stream,packet.light_attn[i])
+ XFCommand.pack(stream,packet.light_color[i])
+ XFCommand.pack(stream,packet.light_dir[i])
+
+ XFCommand.pack(stream,packet.numchans)
+ XFCommand.pack(stream,packet.numtexgens)
+
+
+def pack_packet(stream,packet,material,textures):
+ base = stream.tell()
+
+ packet.texture_offset = stream.tell() - base
+ pack_texture_subpacket(stream,packet,material,textures)
+
+ packet.tev_offset = stream.tell() - base
+ pack_tev_subpacket(stream,packet,material)
+
+ packet.fog_offset = stream.tell() - base
+ pack_fog_subpacket(stream,packet,material)
+
+ packet.texcoord_generator_offset = stream.tell() - base
+ pack_texcoord_generator_subpacket(stream,packet,material)
+
+ packet.channel_color_offset = stream.tell() - base
+ pack_channel_color_subpacket(stream,packet)
+
+ packet.channel_offset = stream.tell() - base
+ pack_channel_subpacket(stream,packet,material)
+
+ align(stream,0x20,b'\x00')
+
+
+def pack(stream,materials,textures):
+ base = stream.tell()
+ header = Header()
+ header.packet_count = len(materials)
+ stream.write(b'\x00'*Header.sizeof())
+
+ packets = [Packet(material,textures) for material in materials]
+ packet_locations = []
+
+ align(stream,0x20)
+ header.packet_offset = stream.tell() - base
+ stream.write(b'\x00'*header.packet_count*PacketLocation.sizeof())
+ align(stream,0x20)
+
+ for i,(packet,material) in enumerate(zip(packets,materials)):
+ packet_base = base + header.packet_offset + i*PacketLocation.sizeof()
+ packet_location = PacketLocation()
+ packet_location.offset = stream.tell() - packet_base
+ pack_packet(stream,packet,material,textures)
+ packet_location.size = stream.tell() - packet_base - packet_location.offset
+ packet_locations.append(packet_location)
+
+ header.subpacket_location_offset = stream.tell() - base
+ for packet in packets:
+ SubpacketLocation.pack(stream,packet)
+
+ header.matrix_index_offset = stream.tell() - base
+ for packet in packets:
+ uint32.pack(stream,packet.mtxidx[0])
+ uint32.pack(stream,packet.mtxidx[1])
+
+ header.unknown0_offset = stream.tell() - base
+ for material in materials:
+ uint8.pack(stream,material.unknown0)
+
+ align(stream,4)
+ header.index_offset = stream.tell() - base
+ for i in range(header.packet_count):
+ uint16.pack(stream,i)
+
+ align(stream,4)
+ header.name_offset = stream.tell() - base
+ j3d.string_table.pack(stream,(material.name for material in materials))
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+
+ stream.seek(base)
+ Header.pack(stream,header)
+
+ stream.seek(base + header.packet_offset)
+ for packet_location in packet_locations:
+ PacketLocation.pack(stream,packet_location)
+
+ stream.seek(base + header.section_size)
+
diff --git a/j3d/model.py b/j3d/model.py
new file mode 100644
index 0000000..1113b01
--- /dev/null
+++ b/j3d/model.py
@@ -0,0 +1,302 @@
+import functools
+import copy
+import numpy
+from OpenGL.GL import *
+from btypes.big_endian import *
+import gl
+import gx
+import gx.texture
+import j3d.inf1
+import j3d.vtx1
+import j3d.evp1
+import j3d.drw1
+import j3d.jnt1
+import j3d.shp1
+import j3d.mat3
+import j3d.mdl3
+import j3d.tex1
+from j3d.opengl import *
+import j3d.vertex_shader
+import j3d.fragment_shader
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ file_type = ByteString(4)
+ file_size = uint32
+ section_count = uint32
+ subversion = ByteString(4)
+ __padding__ = Padding(12)
+
+ def __init__(self):
+ self.magic = b'J3D2'
+
+ @classmethod
+ def pack(cls,stream,header):
+ if header.file_type == b'bmd3':
+ header.section_count = 8
+ elif header.file_type == b'bdl4':
+ header.section_count = 9
+ else:
+ raise ValueError('invalid file type')
+
+ super().pack(stream,header)
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+
+ if header.magic != b'J3D2':
+ raise FormatError('invalid magic')
+
+ if header.file_type == b'bmd3':
+ valid_section_count = 8
+ valid_subversions = {b'SVR3',b'\xFF\xFF\xFF\xFF'}
+ elif header.file_type == b'bdl4':
+ valid_section_count = 9
+ valid_subversions = {b'SVR3'}
+ else:
+ raise FormatError('invalid file type')
+
+ if header.section_count != valid_section_count:
+ raise FormatError('invalid section count')
+
+ if header.subversion not in valid_subversions:
+ raise FormatError('invalid subversion')
+
+ return header
+
+
+def matrix3x4_array_multiply(a,b):
+ c = numpy.empty(a.shape,numpy.float32)
+ numpy.einsum('ijk,ikl->ijl',a[:,:,:3],b[:,:,:3],out=c[:,:,:3])
+ numpy.einsum('ijk,ik->ij',a[:,:,:3],b[:,:,3],out=c[:,:,3])
+ c[:,:,3] += a[:,:,3]
+ return c
+
+
+class GLMatrixIndexArray:
+
+ @staticmethod
+ def field():
+ return (gx.VA_PTNMTXIDX.name,numpy.uint32)
+
+ @staticmethod
+ def load(shape,vertex_array):
+ destination = vertex_array[gx.VA_PTNMTXIDX.name]
+ vertex_index = 0
+ matrix_table = numpy.zeros(10,numpy.uint32)
+
+ for batch in shape.batches:
+ source = numpy.concatenate([primitive.vertices[gx.VA_PTNMTXIDX.name] for primitive in batch.primitives])
+ source //= 3
+
+ for i,index in enumerate(batch.matrix_table):
+ if index == 0xFFFF: continue
+ matrix_table[i] = index
+
+ length = sum(len(primitive.vertices) for primitive in batch.primitives)
+ numpy.take(matrix_table,source,0,destination[vertex_index:vertex_index + length])
+ vertex_index += length
+
+ glEnableVertexAttribArray(MATRIX_INDEX_ATTRIBUTE_LOCATION)
+ vertex_type = vertex_array.dtype
+ stride = vertex_type.itemsize
+ offset = vertex_type.fields[gx.VA_PTNMTXIDX.name][1]
+ glVertexAttribIPointer(MATRIX_INDEX_ATTRIBUTE_LOCATION,1,GL_UNSIGNED_INT,stride,GLvoidp(offset))
+
+
+class GLProgram(gl.Program):
+
+ def __init__(self,vertex_shader,fragment_shader):
+ super().__init__(vertex_shader,fragment_shader)
+
+ glUseProgram(self)
+
+ matrix_block_index = glGetUniformBlockIndex(self,b'MatrixBlock')
+ glUniformBlockBinding(self,matrix_block_index,MATRIX_BLOCK_BINDING_POINT)
+
+ material_block_index = glGetUniformBlockIndex(self,b'MaterialBlock')
+ if material_block_index != GL_INVALID_INDEX:
+ glUniformBlockBinding(self,material_block_index,MATERIAL_BLOCK_BINDING_POINT)
+
+ matrix_table_location = glGetUniformLocation(self,'matrix_table')
+ if matrix_table_location != -1:
+ glUniform1i(matrix_table_location,MATRIX_TABLE_TEXTURE_UNIT)
+
+ for i in range(8):
+ location = glGetUniformLocation(self,'texmap{}'.format(i))
+ if location == -1: continue
+ glUniform1i(location,TEXTURE_UNITS[i])
+
+
+class GLDrawObject:
+
+ def __init__(self,shape,material,program):
+ self.shape = shape
+ self.material = material
+ self.program = program
+
+ @property
+ def hide(self):
+ return self.shape.gl_hide
+
+ def bind(self,textures):
+ self.material.gl_bind(textures)
+ glUseProgram(self.program)
+ self.shape.gl_bind()
+
+ def draw(self):
+ self.shape.gl_draw()
+
+
+class Model:
+
+ def gl_init(self):
+ self.gl_vertex_shader_factory = functools.lru_cache(maxsize=None)(functools.partial(gl.Shader,GL_VERTEX_SHADER))
+ self.gl_fragment_shader_factory = functools.lru_cache(maxsize=None)(functools.partial(gl.Shader,GL_FRAGMENT_SHADER))
+ self.gl_program_factory = functools.lru_cache(maxsize=None)(GLProgram)
+ self.gl_texture_factory = functools.lru_cache(maxsize=None)(gx.texture.GLTexture)
+
+ array_table = {gx.VA_PTNMTXIDX:GLMatrixIndexArray()}
+ array_table.update((attribute,array.gl_convert()) for attribute,array in self.array_table.items())
+
+ for shape in self.shapes:
+ shape.gl_init(array_table)
+
+ for material in self.materials:
+ material.gl_init()
+
+ for texture in self.textures:
+ texture.gl_init(self.gl_texture_factory)
+
+ self.gl_joints = [copy.copy(joint) for joint in self.joints]
+ self.gl_joint_matrices = numpy.empty((len(self.joints),3,4),numpy.float32)
+ self.gl_matrix_table = gl.TextureBuffer(GL_DYNAMIC_DRAW,GL_RGBA32F,(len(self.matrix_descriptors),3,4),numpy.float32)
+ self.gl_update_matrix_table()
+
+ self.gl_draw_objects = list(self.gl_generate_draw_objects(self.scene_graph))
+ self.gl_draw_objects.sort(key=lambda draw_object: draw_object.material.unknown0)
+
+ def gl_create_draw_object(self,shape,material):
+ vertex_shader = self.gl_vertex_shader_factory(j3d.vertex_shader.create_shader_string(material,shape))
+ fragment_shader = self.gl_fragment_shader_factory(j3d.fragment_shader.create_shader_string(material))
+ program = self.gl_program_factory(vertex_shader,fragment_shader)
+ return GLDrawObject(shape,material,program)
+
+ def gl_generate_draw_objects(self,node,parent_material=None):
+ for child in node.children:
+ if child.node_type == j3d.inf1.NodeType.SHAPE:
+ yield self.gl_create_draw_object(self.shapes[child.index],parent_material)
+ yield from self.gl_generate_draw_objects(child,parent_material)
+ elif child.node_type == j3d.inf1.NodeType.MATERIAL:
+ yield from self.gl_generate_draw_objects(child,self.materials[child.index])
+ else:
+ yield from self.gl_generate_draw_objects(child,parent_material)
+
+ def gl_update_joint_matrices(self,node,parent_joint=None,parent_joint_matrix=numpy.eye(3,4,dtype=numpy.float32)):
+ for child in node.children:
+ if child.node_type == j3d.inf1.NodeType.JOINT:
+ joint = self.gl_joints[child.index]
+ joint_matrix = self.gl_joint_matrices[child.index]
+ joint_matrix[:] = joint.create_matrix(parent_joint,parent_joint_matrix)
+ self.gl_update_joint_matrices(child,joint,joint_matrix)
+ else:
+ self.gl_update_joint_matrices(child,parent_joint,parent_joint_matrix)
+
+ def gl_update_matrix_table(self):
+ self.gl_update_joint_matrices(self.scene_graph)
+
+ if self.inverse_bind_matrices is not None:
+ influence_matrices = matrix3x4_array_multiply(self.gl_joint_matrices,self.inverse_bind_matrices)
+
+ for matrix,matrix_descriptor in zip(self.gl_matrix_table,self.matrix_descriptors):
+ if matrix_descriptor.matrix_type == j3d.drw1.MatrixType.JOINT:
+ matrix[:] = self.gl_joint_matrices[matrix_descriptor.index]
+ elif matrix_descriptor.matrix_type == j3d.drw1.MatrixType.INFLUENCE_GROUP:
+ influence_group = self.influence_groups[matrix_descriptor.index]
+ matrix[:] = sum(influence.weight*influence_matrices[influence.index] for influence in influence_group)
+ else:
+ ValueError('invalid matrix type')
+
+ def gl_draw(self):
+ self.gl_matrix_table.bind_texture(MATRIX_TABLE_TEXTURE_UNIT)
+
+ for draw_object in self.gl_draw_objects:
+ if draw_object.hide: continue
+ draw_object.bind(self.textures)
+ draw_object.draw()
+
+
+def skip_section(stream,magic):
+ if stream.read(4) != magic:
+ raise FormatError('invalid magic')
+ section_size = uint32.unpack(stream)
+ stream.seek(section_size - 8,SEEK_CUR)
+
+
+def pack(stream,model,file_type=None):
+ if file_type is None:
+ file_type = model.file_type
+ elif file_type in {'bmd','.bmd'}:
+ file_type = b'bmd3'
+ elif file_type in {'bdl','.bdl'}:
+ file_type = b'bdl4'
+
+ header = Header()
+ header.file_type = file_type
+ header.subversion = model.subversion
+ stream.write(b'\x00'*Header.sizeof())
+
+ shape_batch_count = sum(len(shape.batches) for shape in model.shapes)
+ vertex_position_count = len(model.array_table[gx.VA_POS])
+
+ j3d.inf1.pack(stream,model.scene_graph,shape_batch_count,vertex_position_count)
+ j3d.vtx1.pack(stream,model.array_table)
+ j3d.evp1.pack(stream,model.influence_groups,model.inverse_bind_matrices)
+ j3d.drw1.pack(stream,model.matrix_descriptors)
+ j3d.jnt1.pack(stream,model.joints)
+ j3d.shp1.pack(stream,model.shapes)
+ j3d.mat3.pack(stream,model.materials,model.subversion)
+ if file_type == b'bdl4':
+ j3d.mdl3.pack(stream,model.materials,model.textures)
+ j3d.tex1.pack(stream,model.textures)
+
+ header.file_size = stream.tell()
+ stream.seek(0)
+ Header.pack(stream,header)
+
+
+def unpack(stream):
+ header = Header.unpack(stream)
+ scene_graph,shape_batch_count,vertex_position_count = j3d.inf1.unpack(stream)
+ array_table = j3d.vtx1.unpack(stream)
+ influence_groups,inverse_bind_matrices = j3d.evp1.unpack(stream)
+ matrix_descriptors = j3d.drw1.unpack(stream)
+ joints = j3d.jnt1.unpack(stream)
+ shapes = j3d.shp1.unpack(stream)
+ materials = j3d.mat3.unpack(stream,header.subversion)
+ if header.file_type == b'bdl4':
+ skip_section(stream,b'MDL3')
+ textures = j3d.tex1.unpack(stream)
+
+ array_table[gx.VA_POS] = array_table[gx.VA_POS][0:vertex_position_count]
+
+ if shape_batch_count != sum(len(shape.batches) for shape in shapes):
+ raise FormatError('wrong number of shape batches')
+
+ model = Model()
+ model.file_type = header.file_type
+ model.subversion = header.subversion
+ model.scene_graph = scene_graph
+ model.array_table = array_table
+ model.influence_groups = influence_groups
+ model.inverse_bind_matrices = inverse_bind_matrices
+ model.matrix_descriptors = matrix_descriptors
+ model.joints = joints
+ model.shapes = shapes
+ model.materials = materials
+ model.textures = textures
+
+ return model
+
diff --git a/j3d/opengl.py b/j3d/opengl.py
new file mode 100644
index 0000000..eae2b4e
--- /dev/null
+++ b/j3d/opengl.py
@@ -0,0 +1,37 @@
+import gl
+import gx
+
+
+MATRIX_INDEX_ATTRIBUTE_LOCATION = 0
+POSITION_ATTRIBUTE_LOCATION = 1
+NORMAL_ATTRIBUTE_LOCATION = 2
+BINORMAL_ATTRIBUTE_LOCATION = 3
+TANGENT_ATTRIBUTE_LOCATION = 4
+COLOR_ATTRIBUTE_LOCATION = 5
+TEXCOORD_ATTRIBUTE_LOCATIONS = [6,7,8,9,10,11,12,13]
+
+ATTRIBUTE_LOCATION_TABLE = {
+ gx.VA_PTNMTXIDX:MATRIX_INDEX_ATTRIBUTE_LOCATION,
+ gx.VA_POS:POSITION_ATTRIBUTE_LOCATION,
+ gx.VA_NRM:NORMAL_ATTRIBUTE_LOCATION,
+ gx.VA_CLR0:COLOR_ATTRIBUTE_LOCATION,
+ gx.VA_CLR1:COLOR_ATTRIBUTE_LOCATION,
+ gx.VA_TEX0:TEXCOORD_ATTRIBUTE_LOCATIONS[0],
+ gx.VA_TEX1:TEXCOORD_ATTRIBUTE_LOCATIONS[2],
+ gx.VA_TEX2:TEXCOORD_ATTRIBUTE_LOCATIONS[2],
+ gx.VA_TEX3:TEXCOORD_ATTRIBUTE_LOCATIONS[3],
+ gx.VA_TEX4:TEXCOORD_ATTRIBUTE_LOCATIONS[4],
+ gx.VA_TEX5:TEXCOORD_ATTRIBUTE_LOCATIONS[5],
+ gx.VA_TEX6:TEXCOORD_ATTRIBUTE_LOCATIONS[6],
+ gx.VA_TEX7:TEXCOORD_ATTRIBUTE_LOCATIONS[7]}
+
+MATRIX_BLOCK_BINDING_POINT = 0
+MATERIAL_BLOCK_BINDING_POINT = 1
+MATRIX_TABLE_TEXTURE_UNIT = 0
+TEXTURE_UNITS = [1,2,3,4,5,6,7,8]
+
+
+class MatrixBlock(gl.UniformBlock):
+ projection_matrix = gl.mat4
+ view_matrix = gl.mat4x3
+
diff --git a/j3d/pak1.py b/j3d/pak1.py
new file mode 100644
index 0000000..89718cf
--- /dev/null
+++ b/j3d/pak1.py
@@ -0,0 +1,108 @@
+from btypes.big_endian import *
+from j3d.animation import Animation,select_interpolater,IncompatibleAnimationError
+import j3d.string_table
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ loop_mode = uint8
+ __padding__ = Padding(3)
+ duration = uint16
+ material_animation_count = uint16
+ r_count = uint16
+ g_count = uint16
+ b_count = uint16
+ a_count = uint16
+ material_animation_offset = uint32
+ index_offset = uint32
+ name_offset = uint32
+ r_offset = uint32
+ g_offset = uint32
+ b_offset = uint32
+ a_offset = uint32
+
+
+class Selection(Struct):
+ count = uint16
+ first = uint16
+ unknown0 = uint16
+
+
+class MaterialAnimation(Struct):
+ r = Selection
+ g = Selection
+ b = Selection
+ a = Selection
+
+ def attach(self,material):
+ self.material_color = material.gl_block['material_color0']
+
+ def update(self,time):
+ self.material_color[0] = self.r.interpolate(time)/255
+ self.material_color[1] = self.g.interpolate(time)/255
+ self.material_color[2] = self.b.interpolate(time)/255
+ self.material_color[3] = self.a.interpolate(time)/255
+
+
+class MaterialColorAnimation(Animation):
+
+ def __init__(self,duration,loop_mode,material_animations):
+ super().__init__(duration,loop_mode)
+ self.material_animations = material_animations
+
+ def attach(self,model):
+ for material_animation in self.material_animations:
+ for material in model.materials:
+ if material.name == material_animation.name:
+ material_animation.attach(material)
+ break
+ else:
+ raise IncompatibleAnimationError()
+
+ self.time = -1
+
+ def update_model(self):
+ for material_animation in self.material_animations:
+ material_animation.update(self.time)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+ if header.magic != b'PAK1':
+ raise FormatError('invalid magic')
+
+ stream.seek(base + header.material_animation_offset)
+ material_animations = [MaterialAnimation.unpack(stream) for _ in range(header.material_animation_count)]
+
+ stream.seek(base + header.r_offset)
+ r = [sint16.unpack(stream) for _ in range(header.r_count)]
+
+ stream.seek(base + header.g_offset)
+ g = [sint16.unpack(stream) for _ in range(header.g_count)]
+
+ stream.seek(base + header.b_offset)
+ b = [sint16.unpack(stream) for _ in range(header.b_count)]
+
+ stream.seek(base + header.a_offset)
+ a = [sint16.unpack(stream) for _ in range(header.a_count)]
+
+ stream.seek(base + header.index_offset)
+ for index in range(header.material_animation_count):
+ if index != uint16.unpack(stream):
+ raise FormatError('invalid index')
+
+ stream.seek(base + header.name_offset)
+ names = j3d.string_table.unpack(stream)
+
+ for material_animation,name in zip(material_animations,names):
+ material_animation.r = select_interpolater(material_animation.r,r)
+ material_animation.g = select_interpolater(material_animation.g,g)
+ material_animation.b = select_interpolater(material_animation.b,b)
+ material_animation.a = select_interpolater(material_animation.a,a)
+ material_animation.name = name
+
+ stream.seek(base + header.section_size)
+ return MaterialColorAnimation(header.duration,header.loop_mode,material_animations)
+
diff --git a/j3d/shp1.py b/j3d/shp1.py
new file mode 100644
index 0000000..01c5424
--- /dev/null
+++ b/j3d/shp1.py
@@ -0,0 +1,432 @@
+import numpy
+from btypes.big_endian import *
+import gl
+import gx
+from OpenGL.GL import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ shape_count = uint16
+ __padding__ = Padding(2)
+ shape_offset = uint32
+ index_offset = uint32
+ unknown0_offset = uint32
+ attribute_descriptor_offset = uint32
+ matrix_index_offset = uint32
+ packet_offset = uint32
+ matrix_selection_offset = uint32
+ packet_location_offset = uint32
+
+ def __init__(self):
+ self.magic = b'SHP1'
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'SHP1':
+ raise FormatError('invalid magic')
+ if header.unknown0_offset != 0:
+ logger.warning('unknown0_offset different from default')
+ return header
+
+
+class AttributeDescriptor(Struct):
+ """Arguments to GXSetVtxDesc."""
+ attribute = EnumConverter(uint32,gx.Attribute)
+ input_type = EnumConverter(uint32,gx.InputType)
+
+ def __init__(self,attribute,input_type):
+ self.attribute = attribute
+ self.input_type = input_type
+
+ def field(self):
+ if self.attribute == gx.VA_PTNMTXIDX and self.input_type == gx.DIRECT:
+ return (gx.VA_PTNMTXIDX.name,numpy.uint8)
+ if self.input_type == gx.INDEX8:
+ return (self.attribute.name,numpy.uint8)
+ if self.input_type == gx.INDEX16:
+ return (self.attribute.name,numpy.uint16)
+
+ raise ValueError('invalid attribute descriptor')
+
+
+class AttributeDescriptorList(TerminatedList):
+ element_type = AttributeDescriptor
+ terminator_value = element_type(gx.VA_NULL,gx.NONE)
+
+ @staticmethod
+ def terminator_predicate(element):
+ return element.attribute == gx.VA_NULL
+
+
+class MatrixSelection(Struct):
+ unknown0 = uint16 # position/normal matrix for texture matrices?
+ count = uint16
+ first = uint32
+
+
+class PacketLocation(Struct):
+ size = uint32
+ offset = uint32
+
+
+class Primitive:
+
+ def __init__(self,primitive_type,vertices):
+ self.primitive_type = primitive_type
+ self.vertices = vertices
+
+
+class Batch:
+
+ def __init__(self,primitives,matrix_table,unknown0):
+ self.primitives = primitives
+ self.matrix_table = matrix_table
+ self.unknown0 = unknown0
+
+
+def gl_count_triangles(shape):
+ triangle_count = 0
+
+ for primitive in shape.primitives:
+ if primitive.primitive_type == gx.TRIANGLES:
+ triangle_count += len(primitive.vertices)//3
+ elif primitive.primitive_type == gx.TRIANGLESTRIP:
+ triangle_count += len(primitive.vertices) - 2
+ elif primitive.primitive_type == gx.TRIANGLEFAN:
+ triangle_count += len(primitive.vertices) - 2
+ elif primitive.primitive_type == gx.QUADS:
+ triangle_count += len(primitive.vertices)//2
+ else:
+ raise ValueError('invalid primitive type')
+
+ return triangle_count
+
+
+def gl_create_element_array(shape,element_map,element_count):
+ element_array = numpy.empty(element_count,numpy.uint16)
+
+ element_index = 0
+ vertex_index = 0
+
+ for primitive in shape.primitives:
+ if primitive.primitive_type == gx.TRIANGLES:
+ for i in range(len(primitive.vertices)//3):
+ element_array[element_index + 0] = element_map[vertex_index + 3*i + 0]
+ element_array[element_index + 1] = element_map[vertex_index + 3*i + 2]
+ element_array[element_index + 2] = element_map[vertex_index + 3*i + 1]
+ element_index += 3
+ elif primitive.primitive_type == gx.TRIANGLESTRIP:
+ for i in range(len(primitive.vertices) - 2):
+ element_array[element_index + 0] = element_map[vertex_index + i + 1 - (i % 2)]
+ element_array[element_index + 1] = element_map[vertex_index + i + (i % 2)]
+ element_array[element_index + 2] = element_map[vertex_index + i + 2]
+ element_index += 3
+ elif primitive.primitive_type == gx.TRIANGLEFAN:
+ for i in range(len(primitive.vertices) - 2):
+ element_array[element_index + 0] = element_map[vertex_index]
+ element_array[element_index + 1] = element_map[vertex_index + i + 2]
+ element_array[element_index + 2] = element_map[vertex_index + i + 1]
+ element_index += 3
+ elif primitive.primitive_type == gx.QUADS:
+ for i in range(0,len(primitive.vertices)//4,4):
+ element_array[element_index + 0] = element_map[vertex_index + i]
+ element_array[element_index + 1] = element_map[vertex_index + i + 1]
+ element_array[element_index + 2] = element_map[vertex_index + i + 2]
+ element_array[element_index + 3] = element_map[vertex_index + i + 1]
+ element_array[element_index + 4] = element_map[vertex_index + i + 3]
+ element_array[element_index + 5] = element_map[vertex_index + i + 2]
+ element_index += 6
+ else:
+ raise ValueError('invalid primitive type')
+
+ vertex_index += len(primitive.vertices)
+
+ return element_array
+
+
+class Shape(Struct):
+ transformation_type = uint8
+ __padding__ = Padding(1)
+ batch_count = uint16
+ attribute_descriptor_offset = uint16
+ first_matrix_selection = uint16
+ first_packet = uint16
+ __padding__ = Padding(2)
+ bounding_radius = float32
+ min_x = float32
+ min_y = float32
+ min_z = float32
+ max_x = float32
+ max_y = float32
+ max_z = float32
+
+ def __init__(self):
+ self.transformation_type = 0
+
+ @property
+ def attributes(self):
+ for descriptor in self.attribute_descriptors:
+ yield descriptor.attribute
+
+ @property
+ def primitives(self):
+ for batch in self.batches:
+ yield from batch.primitives
+
+ @classmethod
+ def pack(cls,stream,shape):
+ shape.batch_count = len(shape.batches)
+ super().pack(stream,shape)
+
+ def create_vertex_type(self):
+ return numpy.dtype([descriptor.field() for descriptor in self.attribute_descriptors]).newbyteorder('>')
+
+ def gl_init(self,array_table):
+ self.gl_hide = False
+
+ self.gl_vertex_array = gl.VertexArray()
+ glBindVertexArray(self.gl_vertex_array)
+
+ self.gl_vertex_buffer = gl.Buffer()
+ glBindBuffer(GL_ARRAY_BUFFER,self.gl_vertex_buffer)
+
+ self.gl_element_count = 3*gl_count_triangles(self)
+ self.gl_element_buffer = gl.Buffer()
+ glBindBuffer(GL_ELEMENT_ARRAY_BUFFER,self.gl_element_buffer)
+
+ vertex_type = numpy.dtype([array_table[attribute].field() for attribute in self.attributes])
+ vertex_count = sum(len(primitive.vertices) for primitive in self.primitives)
+ vertex_array = numpy.empty(vertex_count,vertex_type)
+
+ for attribute in self.attributes:
+ array_table[attribute].load(self,vertex_array)
+
+ vertex_array,element_map = numpy.unique(vertex_array,return_inverse=True)
+ element_array = gl_create_element_array(self,element_map,self.gl_element_count)
+
+ glBufferData(GL_ARRAY_BUFFER,vertex_array.nbytes,vertex_array,GL_STATIC_DRAW)
+ glBufferData(GL_ELEMENT_ARRAY_BUFFER,element_array.nbytes,element_array,GL_STATIC_DRAW)
+
+ def gl_bind(self):
+ glBindVertexArray(self.gl_vertex_array)
+
+ def gl_draw(self):
+ glDrawElements(GL_TRIANGLES,self.gl_element_count,GL_UNSIGNED_SHORT,None)
+
+
+def pack_packet(stream,primitives):
+ for primitive in primitives:
+ uint8.pack(stream,primitive.primitive_type)
+ uint16.pack(stream,len(primitive.vertices))
+ primitive.vertices.tofile(stream)
+
+ align(stream,0x20,b'\x00')
+
+
+def unpack_packet(stream,vertex_type,size):
+ # The entire packet is read into memory at once for speed
+ packet = stream.read(size)
+ primitives = []
+ i = 0
+
+ while i < size:
+ opcode = packet[i]
+ if opcode == 0x00:
+ i += 1
+ continue
+ primitive_type = gx.PrimitiveType(opcode)
+ vertex_count = uint16.unpack_from(packet,i + 1)
+ vertices = numpy.frombuffer(packet,vertex_type,vertex_count,i + 3)
+ primitives.append(Primitive(primitive_type,vertices))
+ i += 3 + vertex_count*vertex_type.itemsize
+
+ return primitives
+
+
+class Pool:
+
+ def __init__(self):
+ self.keys = []
+ self.values = []
+
+ def __contains__(self,key):
+ return key in self.keys
+
+ def __missing__(self,key):
+ raise KeyError(key)
+
+ def __getitem__(self,key):
+ try:
+ return self.values[self.keys.index(key)]
+ except ValueError:
+ return self.__missing__(key)
+
+ def __setitem__(self,key,value):
+ try:
+ self.values[self.keys.index(key)] = value
+ except ValueError:
+ self.keys.append(key)
+ self.values.append(value)
+
+
+class CachedOffsetPacker:
+
+ def __init__(self,stream,pack_function,base=0,default_offset_table=None):
+ self.stream = stream
+ self.pack_function = pack_function
+ self.base = base
+ self.offset_table = default_offset_table if default_offset_table is not None else {}
+
+ def __call__(self,*args):
+ if args in self.offset_table:
+ return self.offset_table[args]
+
+ offset = self.stream.tell() - self.base
+ self.pack_function(self.stream,*args)
+ self.offset_table[args] = offset
+ return offset
+
+
+class CachedOffsetUnpacker:
+
+ def __init__(self,stream,unpack_function,base=0):
+ self.stream = stream
+ self.unpack_function = unpack_function
+ self.base = base
+ self.argument_table = {}
+ self.value_table = {}
+
+ def __call__(self,offset,*args):
+ if offset in self.value_table:
+ if args != self.argument_table[offset]:
+ raise ValueError('inconsistent arguments for same offset')
+ return self.value_table[offset]
+
+ self.stream.seek(self.base + offset)
+ value = self.unpack_function(self.stream,*args)
+ self.argument_table[offset] = args
+ self.value_table[offset] = value
+ return value
+
+
+def pack(stream,shapes):
+ base = stream.tell()
+ header = Header()
+ header.shape_count = len(shapes)
+ stream.write(b'\x00'*Header.sizeof())
+
+ header.shape_offset = stream.tell() - base
+ stream.write(b'\x00'*Shape.sizeof()*len(shapes))
+
+ header.index_offset = stream.tell() - base
+ for index in range(len(shapes)):
+ uint16.pack(stream,index)
+
+ align(stream,4)
+ header.unknown0_offset = 0
+
+ align(stream,0x20)
+ header.attribute_descriptor_offset = stream.tell() - base
+ pack_attribute_descriptors = CachedOffsetPacker(stream,AttributeDescriptorList.pack,stream.tell(),Pool())
+ for shape in shapes:
+ shape.attribute_descriptor_offset = pack_attribute_descriptors(shape.attribute_descriptors)
+
+ matrix_indices = []
+ matrix_selections = []
+ packet_locations = []
+
+ for shape in shapes:
+ shape.first_matrix_selection = len(matrix_selections)
+ for batch in shape.batches:
+ matrix_selection = MatrixSelection()
+ matrix_selection.unknown0 = batch.unknown0
+ matrix_selection.first = len(matrix_indices)
+ matrix_selection.count = len(batch.matrix_table)
+ matrix_indices.extend(batch.matrix_table)
+ matrix_selections.append(matrix_selection)
+
+ header.matrix_index_offset = stream.tell() - base
+ for matrix_index in matrix_indices:
+ uint16.pack(stream,matrix_index)
+
+ align(stream,0x20)
+ header.packet_offset = stream.tell() - base
+ for shape in shapes:
+ shape.first_packet_location = len(packet_locations)
+ for batch in shape.batches:
+ packet_location = PacketLocation()
+ packet_location.offset = stream.tell() - header.packet_offset - base
+ pack_packet(stream,batch.primitives)
+ packet_location.size = stream.tell() - packet_location.offset - header.packet_offset - base
+ packet_locations.append(packet_location)
+
+ header.matrix_selection_offset = stream.tell() - base
+ for matrix_selection in matrix_selections:
+ MatrixSelection.pack(stream,matrix_selection)
+
+ header.packet_location_offset = stream.tell() - base
+ for packet_location in packet_locations:
+ PacketLocation.pack(stream,packet_location)
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+
+ stream.seek(base)
+ Header.pack(stream,header)
+
+ stream.seek(base + header.shape_offset)
+ for shape in shapes:
+ Shape.pack(stream,shape)
+
+ stream.seek(base + header.section_size)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+
+ stream.seek(base + header.shape_offset)
+ shapes = [Shape.unpack(stream) for _ in range(header.shape_count)]
+
+ stream.seek(base + header.index_offset)
+ for index in range(header.shape_count):
+ if index != uint16.unpack(stream):
+ raise FormatError('invalid index')
+
+ unpack_attribute_descriptors = CachedOffsetUnpacker(stream,AttributeDescriptorList.unpack,base + header.attribute_descriptor_offset)
+
+ for shape in shapes:
+ shape.attribute_descriptors = unpack_attribute_descriptors(shape.attribute_descriptor_offset)
+
+ stream.seek(base + header.matrix_selection_offset)
+ matrix_selection_count = max(shape.first_matrix_selection + shape.batch_count for shape in shapes)
+ matrix_selections = [MatrixSelection.unpack(stream) for _ in range(matrix_selection_count)]
+
+ stream.seek(base + header.matrix_index_offset)
+ matrix_index_count = max(selection.first + selection.count for selection in matrix_selections)
+ matrix_indices = [uint16.unpack(stream) for _ in range(matrix_index_count)]
+
+ stream.seek(base + header.packet_location_offset)
+ packet_count = max(shape.first_packet + shape.batch_count for shape in shapes)
+ packet_locations = [PacketLocation.unpack(stream) for _ in range(packet_count)]
+
+ for shape in shapes:
+ vertex_type = shape.create_vertex_type()
+ shape.batches = [None]*shape.batch_count
+ for i in range(shape.batch_count):
+ matrix_selection = matrix_selections[shape.first_matrix_selection + i]
+ matrix_table = matrix_indices[matrix_selection.first:matrix_selection.first + matrix_selection.count]
+ packet_location = packet_locations[shape.first_packet + i]
+ stream.seek(base + header.packet_offset + packet_location.offset)
+ primitives = unpack_packet(stream,vertex_type,packet_location.size)
+ shape.batches[i] = Batch(primitives,matrix_table,matrix_selection.unknown0)
+
+ stream.seek(base + header.section_size)
+ return shapes
+
diff --git a/j3d/string_table.py b/j3d/string_table.py
new file mode 100644
index 0000000..62ccf19
--- /dev/null
+++ b/j3d/string_table.py
@@ -0,0 +1,57 @@
+from btypes.big_endian import *
+
+cstring_sjis = CString('shift-jis')
+
+
+class Header(Struct):
+ string_count = uint16
+ __padding__ = Padding(2)
+
+
+class Entry(Struct):
+ string_hash = uint16
+ string_offset = uint16
+
+
+def unsigned_to_signed_byte(b):
+ return b - 0x100 if b & 0x80 else b
+
+
+def calculate_hash(string):
+ h = 0
+ for b in string:
+ h = (h*3 + unsigned_to_signed_byte(b)) & 0xFFFF
+ return h
+
+
+def pack(stream,strings):
+ strings = [string.encode('shift-jis') for string in strings]
+
+ header = Header()
+ header.string_count = len(strings)
+ Header.pack(stream,header)
+
+ offset = Header.sizeof() + Entry.sizeof()*len(strings)
+
+ for string in strings:
+ entry = Entry()
+ entry.string_hash = calculate_hash(string)
+ entry.string_offset = offset
+ Entry.pack(stream,entry)
+ offset += len(string) + 1
+
+ for string in strings:
+ stream.write(string)
+ stream.write(b'\0')
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+ entries = [Entry.unpack(stream) for _ in range(header.string_count)]
+ strings = []
+ for entry in entries:
+ stream.seek(base + entry.string_offset)
+ strings.append(cstring_sjis.unpack(stream))
+ return strings
+
diff --git a/j3d/tex1.py b/j3d/tex1.py
new file mode 100644
index 0000000..33a1eaf
--- /dev/null
+++ b/j3d/tex1.py
@@ -0,0 +1,183 @@
+from btypes.big_endian import *
+import gx
+import gx.texture
+import j3d.string_table
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ texture_count = uint16
+ __padding__ = Padding(2)
+ texture_offset = uint32
+ name_offset = uint32
+
+ def __init__(self):
+ self.magic = b'TEX1'
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'TEX1':
+ raise FormatError('invalid magic')
+ return header
+
+
+class Texture(gx.texture.Texture,Struct):
+ image_format = EnumConverter(uint8,gx.TextureFormat)
+ unknown0 = uint8
+ width = uint16
+ height = uint16
+ wrap_s = EnumConverter(uint8,gx.WrapMode)
+ wrap_t = EnumConverter(uint8,gx.WrapMode)
+ unknown1 = uint8
+ palette_format = EnumConverter(uint8,gx.PaletteFormat)
+ palette_entry_count = uint16
+ palette_offset = uint32
+ use_mipmapping = bool8
+ __padding__ = Padding(3,b'\x00')
+ minification_filter = EnumConverter(uint8,gx.FilterMode)
+ magnification_filter = EnumConverter(uint8,gx.FilterMode)
+ minimum_lod = FixedPointConverter(sint8,1/8)
+ maximum_lod = FixedPointConverter(sint8,1/8)
+ level_count = uint8
+ unknown2 = uint8
+ lod_bias = FixedPointConverter(sint16,1/100)
+ image_offset = uint32
+
+ def __init__(self):
+ super().__init__()
+ self.unknown0 = 1
+ self.unknown1 = 0
+ self.unknown2 = 0
+
+ @classmethod
+ def pack(cls,stream,texture):
+ texture.level_count = len(texture.images)
+ texture.use_mipmapping = texture.minification_filter in {gx.NEAR_MIP_NEAR,gx.LIN_MIP_NEAR,gx.NEAR_MIP_LIN,gx.LIN_MIP_LIN}
+ super().pack(stream,texture)
+
+ @classmethod
+ def unpack(cls,stream):
+ texture = super().unpack(stream)
+ if texture.unknown0 not in {0x00,0x01,0x02,0xCC}:
+ logger.warning('unknown0 different from default')
+ if texture.unknown1 not in {0,1}:
+ logger.warning('unknown1 different from default')
+ return texture
+
+
+class CachedOffsetPacker:
+
+ def __init__(self,stream,pack_function,default_offset_table=None):
+ self.stream = stream
+ self.pack_function = pack_function
+ self.offset_table = default_offset_table if default_offset_table is not None else {}
+
+ def __call__(self,*args):
+ if args in self.offset_table:
+ return self.offset_table[args]
+
+ offset = self.stream.tell()
+ self.pack_function(self.stream,*args)
+ self.offset_table[args] = offset
+ return offset
+
+
+class CachedOffsetUnpacker:
+
+ def __init__(self,stream,unpack_function):
+ self.stream = stream
+ self.unpack_function = unpack_function
+ self.argument_table = {}
+ self.value_table = {}
+
+ def __call__(self,offset,*args):
+ if offset in self.value_table:
+ if args != self.argument_table[offset]:
+ raise ValueError('inconsistent arguments for same offset')
+ return self.value_table[offset]
+
+ self.stream.seek(offset)
+ value = self.unpack_function(self.stream,*args)
+ self.argument_table[offset] = args
+ self.value_table[offset] = value
+ return value
+
+
+def pack(stream,textures):
+ base = stream.tell()
+ header = Header()
+ header.texture_count = len(textures)
+ stream.write(b'\x00'*Header.sizeof())
+
+ align(stream,0x20)
+ header.texture_offset = stream.tell() - base
+ stream.write(b'\x00'*Texture.sizeof()*len(textures))
+
+ pack_palette = CachedOffsetPacker(stream,gx.texture.pack_palette)
+ pack_images = CachedOffsetPacker(stream,gx.texture.pack_images)
+
+ for i,texture in enumerate(textures):
+ texture_offset = base + header.texture_offset + i*Texture.sizeof()
+
+ if texture.palette is None:
+ texture.palette_offset = stream.tell() - texture_offset
+ continue
+
+ texture.palette_offset = pack_palette(texture.palette) - texture_offset
+
+ for i,texture in enumerate(textures):
+ texture_offset = base + header.texture_offset + i*Texture.sizeof()
+ texture.image_offset = pack_images(texture.images) - texture_offset
+
+ header.name_offset = stream.tell() - base
+ j3d.string_table.pack(stream,(texture.name for texture in textures))
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+ stream.seek(base)
+ Header.pack(stream,header)
+
+ stream.seek(base + header.texture_offset)
+ for texture in textures:
+ Texture.pack(stream,texture)
+
+ stream.seek(base + header.section_size)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+
+ stream.seek(base + header.texture_offset)
+ textures = [Texture.unpack(stream) for _ in range(header.texture_count)]
+
+ unpack_palette = CachedOffsetUnpacker(stream,gx.texture.unpack_palette)
+ unpack_images = CachedOffsetUnpacker(stream,gx.texture.unpack_images)
+
+ for i,texture in enumerate(textures):
+ if texture.palette_entry_count == 0:
+ texture.palette = None
+ continue
+
+ texture_offset = base + header.texture_offset + i*Texture.sizeof()
+ palette_offset = texture_offset + texture.palette_offset
+ texture.palette = unpack_palette(palette_offset,texture.palette_format,texture.palette_entry_count)
+
+ for i,texture in enumerate(textures):
+ texture_offset = base + header.texture_offset + i*Texture.sizeof()
+ image_offset = texture_offset + texture.image_offset
+ texture.images = unpack_images(image_offset,texture.image_format,texture.width,texture.height,texture.level_count)
+
+ stream.seek(base + header.name_offset)
+ names = j3d.string_table.unpack(stream)
+ for texture,name in zip(textures,names):
+ texture.name = name
+
+ stream.seek(base + header.section_size)
+ return textures
+
diff --git a/j3d/tpt1.py b/j3d/tpt1.py
new file mode 100644
index 0000000..7d10a6a
--- /dev/null
+++ b/j3d/tpt1.py
@@ -0,0 +1,84 @@
+from btypes.big_endian import *
+from j3d.animation import Animation,IncompatibleAnimationError
+import j3d.string_table
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ loop_mode = uint8
+ __padding__ = Padding(1)
+ duration = uint16
+ material_animation_count = uint16
+ texture_index_count = uint16
+ texture_index_selection_offset = uint32
+ texture_index_offset = uint32
+ material_index_offset = uint32
+ name_offset = uint32
+
+
+class TextureIndexSelection(Struct):
+ count = uint16
+ first = uint16
+ unknown0 = uint8
+ __padding__ = Padding(3)
+
+
+class MaterialAnimation: pass
+
+
+class TextureSwapAnimation(Animation):
+
+ def __init__(self,duration,loop_mode,material_animations):
+ super().__init__(duration,loop_mode)
+ self.material_animations = material_animations
+
+ def attach(self,model):
+ for material_animation in self.material_animations:
+ if material_animation.material_index >= len(model.materials):
+ raise IncompatibleAnimationError()
+ if material_animation.name != model.materials[material_animation.material_index].name:
+ raise IncompatibleAnimationError()
+ if max(material_animation.texture_indices) >= len(model.textures):
+ raise IncompatibleAnimationError()
+
+ self.time = -1
+ self.model = model
+
+ def update_model(self):
+ for material_animation in self.material_animations:
+ if self.time >= len(material_animation.texture_indices):
+ texture_index = material_animation.texture_indices[-1]
+ else:
+ texture_index = material_animation.texture_indices[self.time]
+ self.model.materials[material_animation.material_index].gl_texture_indices[0] = texture_index
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+ if header.magic != b'TPT1':
+ raise FormatError('invalid magic')
+
+ stream.seek(base + header.texture_index_selection_offset)
+ texture_index_selections = [TextureIndexSelection.unpack(stream) for _ in range(header.material_animation_count)]
+
+ stream.seek(base + header.texture_index_offset)
+ texture_indices = [uint16.unpack(stream) for _ in range(header.texture_index_count)]
+
+ stream.seek(base + header.material_index_offset)
+ material_indices = [uint16.unpack(stream) for _ in range(header.material_animation_count)]
+
+ stream.seek(base + header.name_offset)
+ names = j3d.string_table.unpack(stream)
+
+ material_animations = [MaterialAnimation() for _ in range(header.material_animation_count)]
+
+ for material_animation,texture_index_selection,material_index,name in zip(material_animations,texture_index_selections,material_indices,names):
+ material_animation.texture_indices = texture_indices[texture_index_selection.first:texture_index_selection.first + texture_index_selection.count]
+ material_animation.material_index = material_index
+ material_animation.name = name
+
+ stream.seek(base + header.section_size)
+ return TextureSwapAnimation(header.duration,header.loop_mode,material_animations)
+
diff --git a/j3d/trk1.py b/j3d/trk1.py
new file mode 100644
index 0000000..cfde3e4
--- /dev/null
+++ b/j3d/trk1.py
@@ -0,0 +1,164 @@
+from btypes.big_endian import *
+from j3d.animation import Animation,select_interpolater,IncompatibleAnimationError
+import j3d.string_table
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ loop_mode = uint8
+ __padding__ = Padding(1)
+ duration = uint16
+ register_color_animation_count = uint16
+ constant_color_animation_count = uint16
+ register_r_count = uint16
+ register_g_count = uint16
+ register_b_count = uint16
+ register_a_count = uint16
+ constant_r_count = uint16
+ constant_g_count = uint16
+ constant_b_count = uint16
+ constant_a_count = uint16
+ register_color_animation_offset = uint32
+ constant_color_animation_offset = uint32
+ register_index_offset = uint32
+ constant_index_offset = uint32
+ register_name_offset = uint32
+ constant_name_offset = uint32
+ register_r_offset = uint32
+ register_g_offset = uint32
+ register_b_offset = uint32
+ register_a_offset = uint32
+ constant_r_offset = uint32
+ constant_g_offset = uint32
+ constant_b_offset = uint32
+ constant_a_offset = uint32
+
+
+class Selection(Struct):
+ count = uint16
+ first = uint16
+ unknown0 = uint16
+
+
+class ColorAnimation(Struct):
+ r = Selection
+ g = Selection
+ b = Selection
+ a = Selection
+ unknown0 = uint8
+ __padding__ = Padding(3)
+
+ def attach(self,color):
+ self.color = color
+
+ def update(self,time):
+ self.color[0] = self.r.interpolate(time)/255
+ self.color[1] = self.g.interpolate(time)/255
+ self.color[2] = self.b.interpolate(time)/255
+ self.color[3] = self.a.interpolate(time)/255
+
+
+class TevColorAnimation(Animation):
+
+ def __init__(self,duration,loop_mode,register_color_animations,constant_color_animations):
+ super().__init__(duration,loop_mode)
+ self.register_color_animations = register_color_animations
+ self.constant_color_animations = constant_color_animations
+
+ def attach(self,model):
+ for color_animation in self.register_color_animations:
+ for material in model.materials:
+ if material.name == color_animation.name:
+ color_animation.attach(material.gl_block['tev_color{}'.format(color_animation.unknown0)])
+ break
+ else:
+ raise IncompatibleAnimationError()
+
+ for color_animation in self.constant_color_animations:
+ for material in model.materials:
+ if material.name == color_animation.name:
+ color_animation.attach(material.gl_block['kcolor{}'.format(color_animation.unknown0)])
+ break
+ else:
+ raise IncompatibleAnimationError()
+
+ self.time = -1
+
+ def update_model(self):
+ for color_animation in self.register_color_animations:
+ color_animation.update(self.time)
+
+ for color_animation in self.constant_color_animations:
+ color_animation.update(self.time)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+ if header.magic != b'TRK1':
+ raise FormatError('invalid magic')
+
+ stream.seek(base + header.register_color_animation_offset)
+ register_color_animations = [ColorAnimation.unpack(stream) for _ in range(header.register_color_animation_count)]
+
+ stream.seek(base + header.constant_color_animation_offset)
+ constant_color_animations = [ColorAnimation.unpack(stream) for _ in range(header.constant_color_animation_count)]
+
+ stream.seek(base + header.register_r_offset)
+ register_r = [sint16.unpack(stream) for _ in range(header.register_r_count)]
+
+ stream.seek(base + header.register_g_offset)
+ register_g = [sint16.unpack(stream) for _ in range(header.register_g_count)]
+
+ stream.seek(base + header.register_b_offset)
+ register_b = [sint16.unpack(stream) for _ in range(header.register_b_count)]
+
+ stream.seek(base + header.register_a_offset)
+ register_a = [sint16.unpack(stream) for _ in range(header.register_a_count)]
+
+ stream.seek(base + header.constant_r_offset)
+ constant_r = [sint16.unpack(stream) for _ in range(header.constant_r_count)]
+
+ stream.seek(base + header.constant_g_offset)
+ constant_g = [sint16.unpack(stream) for _ in range(header.constant_g_count)]
+
+ stream.seek(base + header.constant_b_offset)
+ constant_b = [sint16.unpack(stream) for _ in range(header.constant_b_count)]
+
+ stream.seek(base + header.constant_a_offset)
+ constant_a = [sint16.unpack(stream) for _ in range(header.constant_a_count)]
+
+ stream.seek(base + header.register_index_offset)
+ for index in range(header.register_color_animation_count):
+ if index != uint16.unpack(stream):
+ raise FormatError('invalid index')
+
+ stream.seek(base + header.constant_index_offset)
+ for index in range(header.constant_color_animation_count):
+ if index != uint16.unpack(stream):
+ raise FormatError('invalid index')
+
+ stream.seek(base + header.register_name_offset)
+ register_names = j3d.string_table.unpack(stream)
+
+ stream.seek(base + header.constant_name_offset)
+ constant_names = j3d.string_table.unpack(stream)
+
+ for color_animation,name in zip(register_color_animations,register_names):
+ color_animation.r = select_interpolater(color_animation.r,register_r)
+ color_animation.g = select_interpolater(color_animation.g,register_g)
+ color_animation.b = select_interpolater(color_animation.b,register_b)
+ color_animation.a = select_interpolater(color_animation.a,register_a)
+ color_animation.name = name
+
+ for color_animation,name in zip(constant_color_animations,constant_names):
+ color_animation.r = select_interpolater(color_animation.r,constant_r)
+ color_animation.g = select_interpolater(color_animation.g,constant_g)
+ color_animation.b = select_interpolater(color_animation.b,constant_b)
+ color_animation.a = select_interpolater(color_animation.a,constant_a)
+ color_animation.name = name
+
+ stream.seek(base + header.section_size)
+ return TevColorAnimation(header.duration,header.loop_mode,register_color_animations,constant_color_animations)
+
diff --git a/j3d/ttk1.py b/j3d/ttk1.py
new file mode 100644
index 0000000..0a1c09a
--- /dev/null
+++ b/j3d/ttk1.py
@@ -0,0 +1,173 @@
+from math import cos,sin,radians
+import numpy
+from btypes.big_endian import *
+import gx
+from j3d.animation import Animation,select_interpolater,IncompatibleAnimationError
+import j3d.string_table
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ loop_mode = uint8
+ angle_scale_exponent = uint8
+ duration = uint16
+ component_animation_count = uint16
+ scale_count = uint16
+ rotation_count = uint16
+ translation_count = uint16
+ component_animation_offset = uint32
+ index_offset = uint32
+ name_offset = uint32
+ texture_matrix_index_offset = uint32
+ center_offset = uint32
+ scale_offset = uint32
+ rotation_offset = uint32
+ translation_offset = uint32
+
+ def __init__(self):
+ self.magic = b'TTK1'
+
+
+class Selection(Struct):
+ count = uint16
+ first = uint16
+ unknown0 = uint16
+
+
+class ComponentAnimation(Struct):
+ scale_selection = Selection
+ rotation_selection = Selection
+ translation_selection = Selection
+
+
+class MatrixAnimation:
+
+ def attach(self,material):
+ self.texture_matrix = material.gl_block['texture_matrix{}'.format(self.texture_matrix_index)]
+ matrix = material.texture_matrices[self.texture_matrix_index]
+ if matrix.shape == gx.TG_MTX2x4:
+ self.row_count = 2
+ elif matrix.shape == gx.TG_MTX3x4:
+ self.row_count = 3
+ else:
+ raise ValueError('invalid texture matrix shape')
+
+ def update(self,time):
+ scale_x = self.scale_x.interpolate(time)
+ scale_y = self.scale_y.interpolate(time)
+ scale_z = self.scale_z.interpolate(time)
+ rotation_x = self.rotation_x.interpolate(time)
+ rotation_y = self.rotation_y.interpolate(time)
+ rotation_z = self.rotation_z.interpolate(time)
+ translation_x = self.translation_x.interpolate(time)
+ translation_y = self.translation_y.interpolate(time)
+ translation_z = self.translation_z.interpolate(time)
+
+ cx = cos(radians(rotation_x))
+ sx = sin(radians(rotation_x))
+ cy = cos(radians(rotation_y))
+ sy = sin(radians(rotation_y))
+ cz = cos(radians(rotation_z))
+ sz = sin(radians(rotation_z))
+
+ R = numpy.matrix([[0,0,0,0],[0,0,0,0],[0,0,0,0],[0,0,0,1.0]]) #<-?
+ R[0,0] = cy*cz
+ R[0,1] = (sx*sy*cz - cx*sz)
+ R[0,2] = (cx*sy*cz + sx*sz)
+ R[1,0] = cy*sz
+ R[1,1] = (sx*sy*sz + cx*cz)
+ R[1,2] = (cx*sy*sz - sx*cz)
+ R[2,0] = -sy
+ R[2,1] = sx*cy
+ R[2,2] = cx*cy
+
+ S = numpy.matrix([[scale_x,0,0,0],[0,scale_y,0,0],[0,0,scale_z,0],[0,0,0,1]])
+ C = numpy.matrix([[1,0,0,self.center_x],[0,1,0,self.center_y],[0,0,1,self.center_z],[0,0,0,1]])
+ T = numpy.matrix([[1,0,0,translation_x],[0,1,0,translation_y],[0,0,1,translation_z],[0,0,0,1]])
+
+ self.texture_matrix[:] = (T*C*S*R*C.I)[:self.row_count,:]
+
+
+class TextureMatrixAnimation(Animation):
+
+ def __init__(self,duration,loop_mode,texture_matrix_animations):
+ super().__init__(duration,loop_mode)
+ self.texture_matrix_animations = texture_matrix_animations
+
+ def attach(self,model):
+ for texture_matrix_animation in self.texture_matrix_animations:
+ for material in model.materials:
+ if material.name == texture_matrix_animation.material_name:
+ texture_matrix_animation.attach(material)
+ break
+ else:
+ raise IncompatibleAnimationError()
+
+ self.time = -1
+
+ def update_model(self):
+ for texture_matrix_animation in self.texture_matrix_animations:
+ texture_matrix_animation.update(self.time)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+ if header.magic != b'TTK1':
+ raise FormatError('invalid magic')
+
+ stream.seek(base + header.component_animation_offset)
+ component_animations = [ComponentAnimation.unpack(stream) for _ in range(header.component_animation_count)]
+
+ stream.seek(base + header.index_offset)
+ for index in range(header.component_animation_count//3):
+ if index != uint16.unpack(stream):
+ raise FormatError('invalid index')
+
+ stream.seek(base + header.name_offset)
+ names = j3d.string_table.unpack(stream)
+
+ stream.seek(base + header.texture_matrix_index_offset)
+ texture_matrix_indices = [uint8.unpack(stream) for _ in range(header.component_animation_count//3)]
+
+ stream.seek(base + header.center_offset)
+ centers = [float32.unpack(stream) for _ in range(header.component_animation_count)]
+
+ stream.seek(base + header.scale_offset)
+ scales = [float32.unpack(stream) for _ in range(header.scale_count)]
+
+ stream.seek(base + header.rotation_offset)
+ rotations = [sint16.unpack(stream) for _ in range(header.rotation_count)]
+
+ stream.seek(base + header.translation_offset)
+ translations = [float32.unpack(stream) for _ in range(header.translation_count)]
+
+ angle_scale = 180/32767*2**header.angle_scale_exponent
+
+ for component_animation in component_animations:
+ component_animation.scale = select_interpolater(component_animation.scale_selection,scales)
+ component_animation.rotation = select_interpolater(component_animation.rotation_selection,rotations,angle_scale)
+ component_animation.translation = select_interpolater(component_animation.translation_selection,translations)
+
+ texture_matrix_animations = [MatrixAnimation() for _ in range(header.component_animation_count//3)]
+
+ for i,texture_matrix_animation in enumerate(texture_matrix_animations):
+ texture_matrix_animation.material_name = names[i]
+ texture_matrix_animation.texture_matrix_index = texture_matrix_indices[i]
+ texture_matrix_animation.center_x = centers[3*i]
+ texture_matrix_animation.center_y = centers[3*i + 1]
+ texture_matrix_animation.center_z = centers[3*i + 2]
+ texture_matrix_animation.scale_x = component_animations[3*i].scale
+ texture_matrix_animation.scale_y = component_animations[3*i + 1].scale
+ texture_matrix_animation.scale_z = component_animations[3*i + 2].scale
+ texture_matrix_animation.rotation_x = component_animations[3*i].rotation
+ texture_matrix_animation.rotation_y = component_animations[3*i + 1].rotation
+ texture_matrix_animation.rotation_z = component_animations[3*i + 2].rotation
+ texture_matrix_animation.translation_x = component_animations[3*i].translation
+ texture_matrix_animation.translation_y = component_animations[3*i + 1].translation
+ texture_matrix_animation.translation_z = component_animations[3*i + 2].translation
+
+ stream.seek(base + header.section_size)
+ return TextureMatrixAnimation(header.duration,header.loop_mode,texture_matrix_animations)
+
diff --git a/j3d/vaf1.py b/j3d/vaf1.py
new file mode 100644
index 0000000..23c52fb
--- /dev/null
+++ b/j3d/vaf1.py
@@ -0,0 +1,65 @@
+from btypes.big_endian import *
+from j3d.animation import Animation,IncompatibleAnimationError
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ loop_mode = uint8
+ __padding__ = Padding(1)
+ duration = uint16
+ shape_animation_count = uint16
+ show_count = uint16
+ show_selection_offset = uint32
+ show_offset = uint32
+
+
+class ShowSelection(Struct):
+ count = uint16
+ first = uint16
+
+
+class ShapeAnimation: pass
+
+
+class ShapeVisibilityAnimation(Animation):
+
+ def __init__(self,duration,loop_mode,shape_animations):
+ super().__init__(duration,loop_mode)
+ self.shape_animations = shape_animations
+
+ def attach(self,model):
+ if len(self.shape_animations) != len(model.shapes):
+ raise IncompatibleAnimationError()
+ self.time = -1
+ self.model = model
+
+ def update_model(self):
+ for shape,shape_animation in zip(self.model.shapes,self.shape_animations):
+ if self.time >= len(shape_animation.shows):
+ show = shape_animation.shows[-1]
+ else:
+ show = shape_animation.shows[self.time]
+ shape.hide = not show
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+ if header.magic != b'VAF1':
+ raise FormatError('invalid magic')
+
+ stream.seek(base + header.show_selection_offset)
+ show_selections = [ShowSelection.unpack(stream) for _ in range(header.shape_animation_count)]
+
+ stream.seek(base + header.show_offset)
+ shows = [bool8.unpack(stream) for _ in range(header.show_count)]
+
+ shape_animations = [ShapeAnimation() for _ in range(header.shape_animation_count)]
+
+ for shape_animation,show_selection in zip(shape_animations,show_selections):
+ shape_animation.shows = shows[show_selection.first:show_selection.first + show_selection.count]
+
+ stream.seek(base + header.section_size)
+ return ShapeVisibilityAnimation(header.duration,header.loop_mode,shape_animations)
+
diff --git a/j3d/vertex_shader.py b/j3d/vertex_shader.py
new file mode 100644
index 0000000..2d91a08
--- /dev/null
+++ b/j3d/vertex_shader.py
@@ -0,0 +1,173 @@
+from io import StringIO
+from OpenGL.GL import *
+import gx
+from j3d.opengl import *
+
+
+def convert_material_source(source,index):
+ if source == gx.SRC_VTX:
+ return 'color'
+ elif source == gx.SRC_REG:
+ return 'material_color{}'.format(index)
+ else:
+ raise ValueError('invalid material source')
+
+
+def convert_ambient_source(source,index):
+ if source == gx.SRC_VTX:
+ return 'color'
+ elif source == gx.SRC_REG:
+ return 'ambient_color{}'.format(index)
+ else:
+ raise ValueError('invalid ambient source')
+
+
+def write_channel(stream,index,channel):
+ material_color = convert_material_source(channel.color_mode.material_source,index)
+ material_alpha = convert_material_source(channel.alpha_mode.material_source,index)
+
+ stream.write('channel{} = vec4('.format(index))
+
+ #XXX Lighting can't be properly implemented as BMD/BDL files doesn't
+ # store any light information, but this seems to work pretty well
+ if channel.color_mode.light_enable:
+ ambient_color = convert_ambient_source(channel.color_mode.ambient_source,index)
+ stream.write('0.5*({}.rgb + vec3(1.0))*'.format(ambient_color))
+ #stream.write('clamp({}.rgb + vec3(0.3),0.0,1.0)*'.format(ambient_color))
+
+ stream.write('{}.rgb,{}.a);\n'.format(material_color,material_alpha))
+
+
+def write_identity_texcoord_generator(stream,generator):
+ if generator.source == gx.TG_POS:
+ source = 'position'
+ elif generator.source == gx.TG_NRM:
+ source = 'normal'
+ elif generator.source == gx.TG_BINRM:
+ source = 'binormal'
+ elif generator.source == gx.TG_TANGENT:
+ source = 'tangent'
+ elif generator.source in gx.TG_TEX:
+ source = 'texcoord{}'.format(generator.source.index)
+ else:
+ raise ValueError('invalid texture coordinate generator source')
+
+ stream.write(source)
+ if generator.function == gx.TG_MTX2x4 and generator.source not in gx.TG_TEX:
+ stream.write('.xy')
+ elif generator.function == gx.TG_MTX3x4 and generator.source == gx.TG_POS:
+ stream.write('.xyz')
+
+
+def write_matrix_texcoord_generator(stream,generator,texture_matrices):
+ if generator.source == gx.TG_POS:
+ source = 'position'
+ elif generator.source == gx.TG_NRM:
+ source = 'vec4(normal,1.0)'
+ elif generator.source == gx.TG_BINRM:
+ source = 'vec4(binormal,1.0)'
+ elif generator.source == gx.TG_TANGENT:
+ source = 'vec4(tangent,1.0)'
+ elif generator.source in gx.TG_TEX:
+ source = 'vec4(texcoord{},1.0,1.0)'.format(generator.source.index)
+ else:
+ raise ValueError('invalid texture coordinate generator source')
+
+ matrix_index = generator.matrix.index
+ matrix = texture_matrices[matrix_index]
+
+ if matrix.shape != generator.function:
+ raise ValueError() #<-?
+
+ stream.write('texture_matrix{}*'.format(matrix_index))
+ if matrix.matrix_type in {0x06,0x07}:
+ stream.write('vec4(view_matrix*vec4({}.xyz,0.0),1.0)'.format(source))
+ elif matrix.matrix_type == 0x09:
+ stream.write('vec4(view_matrix*{},1.0)'.format(source))
+ else:
+ stream.write(source)
+
+
+def write_texcoord_generator(stream,index,generator,texture_matrices):
+ stream.write('generated_texcoord{} = '.format(index))
+
+ if generator.function in {gx.TG_MTX2x4,gx.TG_MTX3x4}:
+ if generator.matrix == gx.IDENTITY:
+ write_identity_texcoord_generator(stream,generator)
+ else:
+ write_matrix_texcoord_generator(stream,generator,texture_matrices)
+ elif generator.function in gx.TG_BUMP:
+ stream.write('generated_texcoord{}.st'.format(generator.source.index))
+ elif generator.function == gx.TG_SRTG:
+ stream.write('channel{}.rg'.format(generator.source.index))
+ else:
+ raise ValueError('invalid texture coordinate generator function')
+
+ stream.write(';\n')
+
+
+def create_shader_string(material,shape):
+ stream = StringIO()
+
+ stream.write('#version 330\n')
+
+ stream.write('{}\n'.format(MatrixBlock.glsl_type))
+ stream.write('{}\n'.format(material.gl_block.glsl_type))
+
+ if shape.transformation_type == 0:
+ stream.write('const int matrix_index = {};\n'.format(shape.batches[0].matrix_table[0]))
+ stream.write('uniform samplerBuffer matrix_table;\n')
+ stream.write('#define MATRIX_ROW(i) texelFetch(matrix_table,3*matrix_index + i)\n')
+ position = 'view_matrix*vec4(dot(MATRIX_ROW(0),position),dot(MATRIX_ROW(1),position),dot(MATRIX_ROW(2),position),1.0)'
+ elif shape.transformation_type == 1:
+ position = '(position.xyz + view_matrix[3])'
+ elif shape.transformation_type == 2:
+ raise Exception('y billboard matrix not implemented') #TODO
+ elif shape.transformation_type == 3:
+ stream.write('layout(location={}) in int matrix_index;\n'.format(MATRIX_INDEX_ATTRIBUTE_LOCATION))
+ stream.write('uniform samplerBuffer matrix_table;\n')
+ stream.write('#define MATRIX_ROW(i) texelFetch(matrix_table,3*matrix_index + i)\n')
+ position = 'view_matrix*vec4(dot(MATRIX_ROW(0),position),dot(MATRIX_ROW(1),position),dot(MATRIX_ROW(2),position),1.0)'
+ else:
+ raise ValueError('invalid matrix type')
+
+ stream.write('layout(location={}) in vec4 position;\n'.format(POSITION_ATTRIBUTE_LOCATION))
+
+ if material.use_normal:
+ stream.write('layout(location={}) in vec3 normal;\n'.format(NORMAL_ATTRIBUTE_LOCATION))
+
+ if material.use_binormal:
+ stream.write('layout(location={}) in vec3 binormal;\n'.format(BINORMAL_ATTRIBUTE_LOCATION))
+
+ if material.use_tangent:
+ stream.write('layout(location={}) in vec3 tangent;\n'.format(TANGENT_ATTRIBUTE_LOCATION))
+
+ if material.use_color:
+ stream.write('layout(location={}) in vec4 color;\n'.format(COLOR_ATTRIBUTE_LOCATION))
+
+ for i in range(8):
+ if not material.use_texcoord[i]: continue
+ stream.write('layout(location={}) in vec2 texcoord{};\n'.format(TEXCOORD_ATTRIBUTE_LOCATIONS[i],i))
+
+ for i,channel in enumerate(material.enabled_channels):
+ stream.write('out vec4 channel{};\n'.format(i))
+
+ for i,generator in enumerate(material.enabled_texcoord_generators):
+ if generator.function == gx.TG_MTX3x4 and not (generator.source in gx.TG_TEX and generator.matrix == gx.IDENTITY): #<-?
+ stream.write('out vec3 generated_texcoord{};\n'.format(i))
+ else:
+ stream.write('out vec2 generated_texcoord{};\n'.format(i))
+
+ stream.write('\nvoid main()\n{\n')
+ stream.write('gl_Position = projection_matrix*vec4({},1.0);\n'.format(position))
+
+ for i,channel in enumerate(material.enabled_channels):
+ write_channel(stream,i,channel)
+
+ for i,generator in enumerate(material.enabled_texcoord_generators):
+ write_texcoord_generator(stream,i,generator,material.texture_matrices)
+
+ stream.write('}\n')
+
+ return stream.getvalue()
+
diff --git a/j3d/vtx1.py b/j3d/vtx1.py
new file mode 100644
index 0000000..c9d7eed
--- /dev/null
+++ b/j3d/vtx1.py
@@ -0,0 +1,234 @@
+from collections import defaultdict
+import numpy
+from OpenGL.GL import *
+from btypes.big_endian import *
+import gx
+from j3d.opengl import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Header(Struct):
+ magic = ByteString(4)
+ section_size = uint32
+ attribute_format_offset = uint32
+ position_offset = uint32
+ normal_offset = uint32
+ unknown0_offset = uint32 # NBT?
+ color_offsets = Array(uint32,2)
+ texcoord_offsets = Array(uint32,8)
+
+ def __init__(self):
+ self.magic = b'VTX1'
+ self.unknown0_offset = 0
+
+ @classmethod
+ def unpack(cls,stream):
+ header = super().unpack(stream)
+ if header.magic != b'VTX1':
+ raise FormatError('invalid magic')
+ if header.unknown0_offset != 0:
+ logger.warning('unknown0_offset different from default')
+ return header
+
+
+class AttributeFormat(Struct):
+ """ Arguments to GXSetVtxAttrFmt."""
+ attribute = EnumConverter(uint32,gx.Attribute)
+ component_count = uint32
+ component_type = uint32
+ scale_exponent = uint8
+ __padding__ = Padding(3)
+
+ def __init__(self,attribute,component_count,component_type,scale_exponent):
+ self.attribute = attribute
+ self.component_count = component_count
+ self.component_type = component_type
+ self.scale_exponent = scale_exponent
+
+
+class AttributeFormatList(TerminatedList):
+ element_type = AttributeFormat
+ terminator_value = AttributeFormat(gx.VA_NULL,1,0,0)
+
+ @staticmethod
+ def terminator_predicate(element):
+ return element.attribute == gx.VA_NULL
+
+
+class Array(numpy.ndarray):
+
+ @staticmethod
+ def create_element_type(component_type,component_count):
+ return numpy.dtype((component_type.numpy_type,component_count.actual_value)).newbyteorder('>')
+
+ def __array_finalize__(self,obj):
+ if not isinstance(obj,Array): return
+ self.attribute = obj.attribute
+ self.component_type = obj.component_type
+ self.component_count = obj.component_count
+ self.scale_exponent = obj.scale_exponent
+
+ def gl_convert(self):
+ array = numpy.asfarray(self,numpy.float32)
+
+ if self.component_type != gx.F32 and self.scale_exponent != 0:
+ array *= 2**(-self.scale_exponent)
+
+ array = array.view(GLArray)
+ array.attribute = self.attribute
+ array.component_type = GL_FLOAT
+ array.component_count = self.shape[1]
+ array.normalize = False
+ return array
+
+
+class ColorArray(numpy.ndarray):
+
+ @property
+ def has_alpha(self):
+ return self.component_count == gx.CLR_RGBA and self.component_type in {gx.RGBA8,gx.RGBA4,gx.RGBA6}
+
+ @staticmethod
+ def create_element_type(component_type,component_count):
+ if component_type in {gx.RGB8,gx.RGBX8,gx.RGBA8}:
+ return numpy.dtype((numpy.uint8,4))
+ if component_type in {gx.RGB565,gx.RGBA4}:
+ return numpy.dtype(numpy.uint16).newbyteorder('>')
+ if component_type == gx.RGBA6:
+ return numpy.dtype(numpy.uint32).newbyteorder('>')
+
+ raise ValueError('invalid color component type')
+
+ def gl_convert(self):
+ if self.component_type in {gx.RGB8,gx.RGBX8,gx.RGBA8}:
+ array = self
+
+ if self.component_type == gx.RGB565:
+ array = numpy.empty((element_count,4),numpy.uint8)
+ array[:,0] = ((self >> 8) & 0xF8) | ((self >> 13) & 0x7)
+ array[:,1] = ((self >> 3) & 0xFC) | ((self >> 9) & 0x3)
+ array[:,2] = ((self << 3) & 0xF8) | ((self >> 2) & 0x7)
+ array[:,3] = 0xFF
+
+ if self.component_type == gx.RGBA4:
+ array = numpy.empty((element_count,4),numpy.uint8)
+ array[:,0] = ((self >> 8) & 0xF0) | ((self >> 12) & 0xF)
+ array[:,1] = ((self >> 4) & 0xF0) | ((self >> 8) & 0xF)
+ array[:,2] = (self & 0xF0) | ((self >> 4) & 0xF)
+ array[:,3] = ((self << 4) & 0xF0) | (self & 0xF)
+
+ if self.component_type == gx.RGBA6:
+ array = numpy.empty((element_count,4),numpy.uint8)
+ array[:,0] = ((self >> 16) & 0xFC) | ((self >> 22) & 0x3)
+ array[:,1] = ((self >> 10) & 0xFC) | ((self >> 16) & 0x3)
+ array[:,2] = ((self >> 4) & 0xFC) | ((self >> 10) & 0x3)
+ array[:,3] = ((self << 2) & 0xFC) | ((self >> 4) & 0x3)
+
+ array = array.view(GLArray)
+ array.attribute = self.attribute
+ array.component_type = GL_UNSIGNED_BYTE
+ array.component_count = 4 if self.has_alpha else 3
+ array.normalize = True
+ return array
+
+
+class GLArray(numpy.ndarray):
+
+ def field(self):
+ return (self.attribute.name,self.dtype,self.shape[1])
+
+ def load(self,shape,vertex_array):
+ index_array = numpy.concatenate([primitive.vertices[self.attribute.name] for primitive in shape.primitives])
+ numpy.take(self,index_array,0,vertex_array[self.attribute.name])
+ location = ATTRIBUTE_LOCATION_TABLE[self.attribute]
+ glEnableVertexAttribArray(location)
+ vertex_type = vertex_array.dtype
+ stride = vertex_type.itemsize
+ offset = vertex_type.fields[self.attribute.name][1]
+ glVertexAttribPointer(location,self.component_count,self.component_type,self.normalize,stride,GLvoidp(offset))
+
+
+def unpack_array(stream,attribute_format,size):
+ if attribute_format.attribute == gx.VA_POS:
+ component_type = gx.ComponentType(attribute_format.component_type)
+ component_count = gx.PositionComponentCount(attribute_format.component_count)
+ array_type = Array
+ elif attribute_format.attribute == gx.VA_NRM:
+ component_type = gx.ComponentType(attribute_format.component_type)
+ component_count = gx.NormalComponentCount(attribute_format.component_count)
+ array_type = Array
+ elif attribute_format.attribute in gx.VA_CLR:
+ component_type = gx.ColorComponentType(attribute_format.component_type)
+ component_count = gx.ColorComponentCount(attribute_format.component_count)
+ array_type = ColorArray
+ elif attribute_format.attribute in gx.VA_TEX:
+ component_type = gx.ComponentType(attribute_format.component_type)
+ component_count = gx.TexCoordComponentCount(attribute_format.component_count)
+ array_type = Array
+ else:
+ raise FormatError('invalid vertex attribute')
+
+ element_type = array_type.create_element_type(component_type,component_count)
+ element_count = size//element_type.itemsize
+ array = numpy.fromfile(stream,element_type,element_count).view(array_type)
+ array.attribute = attribute_format.attribute
+ array.component_type = component_type
+ array.component_count = component_count
+ array.scale_exponent = attribute_format.scale_exponent
+ return array
+
+
+def pack(stream,array_table):
+ base = stream.tell()
+ header = Header()
+ stream.write(b'\x00'*Header.sizeof())
+
+ offset_table = defaultdict(int)
+ arrays = [array_table[attribute] for attribute in gx.Attribute if attribute in array_table]
+
+ header.attribute_format_offset = stream.tell() - base
+ AttributeFormatList.pack(stream,arrays)
+
+ for array in arrays:
+ align(stream,0x20)
+ offset_table[array.attribute] = stream.tell() - base
+ array.tofile(stream)
+
+ header.position_offset = offset_table[gx.VA_POS]
+ header.normal_offset = offset_table[gx.VA_NRM]
+ header.color_offsets = [offset_table[attribute] for attribute in gx.VA_CLR]
+ header.texcoord_offsets = [offset_table[attribute] for attribute in gx.VA_TEX]
+
+ align(stream,0x20)
+ header.section_size = stream.tell() - base
+ stream.seek(base)
+ Header.pack(stream,header)
+ stream.seek(base + header.section_size)
+
+
+def unpack(stream):
+ base = stream.tell()
+ header = Header.unpack(stream)
+
+ offset_table = {}
+ array_table = {}
+
+ offset_table[gx.VA_POS] = header.position_offset
+ offset_table[gx.VA_NRM] = header.normal_offset
+ offset_table.update(zip(gx.VA_CLR,header.color_offsets))
+ offset_table.update(zip(gx.VA_TEX,header.texcoord_offsets))
+
+ stream.seek(base + header.attribute_format_offset)
+ attribute_formats = AttributeFormatList.unpack(stream)
+
+ for attribute_format in attribute_formats:
+ array_offset = offset_table[attribute_format.attribute]
+ size = min((offset for offset in offset_table.values() if offset > array_offset),default=header.section_size) - array_offset
+ stream.seek(base + array_offset)
+ array_table[attribute_format.attribute] = unpack_array(stream,attribute_format,size)
+
+ stream.seek(base + header.section_size)
+ return array_table
+
diff --git a/j3dview.py b/j3dview.py
new file mode 100644
index 0000000..6e4ecc5
--- /dev/null
+++ b/j3dview.py
@@ -0,0 +1,283 @@
+#!/usr/bin/env python3
+
+if __name__ == '__main__':
+ # Logging and OpenGL have to be configured before OpenGL.GL is imported
+ import io
+ import logging
+ import OpenGL
+
+ stderr_handler = logging.StreamHandler()
+ stderr_handler.setLevel(logging.WARNING)
+ logging_stream = io.StringIO()
+ logging_stream_handler = logging.StreamHandler(logging_stream)
+ logging.basicConfig(level=logging.DEBUG,handlers=[stderr_handler,logging_stream_handler])
+
+ OpenGL.ERROR_ON_COPY = True
+ OpenGL.STORE_POINTERS = False
+ OpenGL.FORWARD_COMPATIBLE_ONLY = True
+
+import os.path
+import numpy
+from OpenGL.GL import *
+from PyQt4 import QtCore,QtGui,QtOpenGL,uic
+import j3d.model
+import j3d.animation
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class PreviewWidget(QtOpenGL.QGLWidget):
+
+ def __init__(self,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+ self.texture = None
+
+ @QtCore.pyqtSlot(object)
+ def setTexture(self,texture):
+ self.texture = texture
+ self.updateGL()
+
+ def paintGL(self):
+ glClearColor(0,0,0,1)
+ glClear(GL_COLOR_BUFFER_BIT)
+ if self.texture is not None and self.height() != 0 and self.width() != 0:
+ s = self.width()/self.height()*self.texture.height/self.texture.width
+ if s < 1:
+ self.drawTexture(QtCore.QRectF(-1,-s,2,2*s),self.texture.gl_texture)
+ else:
+ s = self.height()/self.width()*self.texture.width/self.texture.height
+ self.drawTexture(QtCore.QRectF(-s,-1,2*s,2),self.texture.gl_texture)
+
+ def resizeGL(self,width,height):
+ glViewport(0,0,width,height)
+
+
+class Editor(QtGui.QMainWindow):
+
+ def __init__(self,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+
+ self.undo_stack = QtGui.QUndoStack(self,objectName='undo_stack')
+ self.action_undo = self.undo_stack.createUndoAction(self)
+ self.action_redo = self.undo_stack.createRedoAction(self)
+
+ self.ui = uic.loadUi('ui/Editor.ui',self)
+
+ self.menu_edit.addAction(self.action_undo)
+ self.menu_edit.addAction(self.action_redo)
+
+ self.menu_window.addAction(self.dock_view_settings.toggleViewAction())
+ self.menu_window.addAction(self.dock_explorer.toggleViewAction())
+ self.menu_window.addAction(self.dock_preview.toggleViewAction())
+ self.menu_window.addAction(self.dock_texture.toggleViewAction())
+
+ self.action_open_model.setShortcut(QtGui.QKeySequence.Open)
+ self.action_save_model.setShortcut(QtGui.QKeySequence.Save)
+ self.action_save_model_as.setShortcut(QtGui.QKeySequence.SaveAs)
+ self.action_quit.setShortcut(QtGui.QKeySequence.Quit)
+ self.action_undo.setShortcut(QtGui.QKeySequence.Undo)
+ self.action_redo.setShortcut(QtGui.QKeySequence.Redo)
+
+ self.view_settings.setViewer(self.viewer)
+ self.dock_view_settings.hide()
+
+ self.preview = PreviewWidget(shareWidget=self.viewer)
+ self.dock_preview.setWidget(self.preview)
+
+ self.texture.setUndoStack(self.undo_stack)
+
+ self.action_open_animation.setEnabled(False)
+ self.action_save_model.setEnabled(False)
+ self.action_save_model_as.setEnabled(False)
+
+ self.setWindowFilePath('')
+
+ self.adjustSize()
+
+ #self.readSettings()
+
+ def windowFilePath(self):
+ if not self.has_window_file_path:
+ return ''
+ return super().windowFilePath()
+
+ def setWindowFilePath(self,path):
+ if path == '':
+ self.has_window_file_path = False
+ super().setWindowFilePath('[No File]')
+ else:
+ self.has_window_file_path = True
+ super().setWindowFilePath(path)
+
+ def warning(self,message):
+ QtGui.QMessageBox.warning(self,QtGui.qApp.applicationName(),message)
+
+ def load(self,file_name):
+ try:
+ with open(file_name,'rb') as stream:
+ model = j3d.model.unpack(stream)
+ except (FileNotFoundError,IsADirectoryError,PermissionError) as error:
+ self.warning('Could not open file \'{}\': {}'.format(file_name,error.strerror))
+ return
+
+ self.undo_stack.clear()
+
+ self.model = model
+ self.viewer.setModel(model)
+ self.explorer.setModel(model)
+
+ self.action_open_animation.setEnabled(True)
+ self.action_save_model.setEnabled(True)
+ self.action_save_model_as.setEnabled(True)
+
+ self.setWindowFilePath(file_name)
+
+ def save(self,file_name):
+ try:
+ with open(file_name,'wb') as stream:
+ j3d.model.pack(stream,self.model,os.path.splitext(file_name)[1].lower())
+ except (FileNotFoundError,IsADirectoryError,PermissionError) as error:
+ self.warning('Could not save file \'{}\': {}'.format(file_name,error.strerror))
+ return
+
+ self.undo_stack.setClean()
+ self.setWindowFilePath(file_name)
+
+ def loadAnimation(self,file_name):
+ try:
+ with open(file_name,'rb') as stream:
+ animation = j3d.animation.unpack(stream)
+ except (FileNotFoundError,IsADirectoryError,PermissionError) as error:
+ self.warning('Could not open file \'{}\': {}'.format(file_name,error.strerror))
+ return
+
+ try:
+ self.viewer.setAnimation(animation)
+ except j3d.animation.IncompatibleAnimationError:
+ self.warning('Incompatible animation')
+
+ def writeSettings(self):
+ settings = QtCore.QSettings()
+
+ settings.setValue('geometry',self.saveGeometry())
+ settings.setValue('state',self.saveState())
+
+ settings.beginGroup('view_settings')
+ settings.setValue('z_near',self.viewer.z_near)
+ settings.setValue('z_far',self.viewer.z_far)
+ settings.setValue('fov',self.viewer.fov)
+ settings.setValue('movement_speed',self.viewer.movement_speed)
+ settings.setValue('rotation_speed',self.viewer.rotation_speed)
+ settings.endGroup()
+
+ def readSettings(self):
+ settings = QtCore.QSettings()
+
+ geometry = settings.value('geometry')
+ if geometry is not None:
+ self.restoreGeometry(geometry)
+
+ state = settings.value('state')
+ if state is not None:
+ self.restoreState(state) #FIXME: Use versioning
+
+ settings.beginGroup('view_settings')
+ self.viewer.z_near = settings.value('z_near',25,float)
+ self.viewer.z_far = settings.value('z_far',12800,float)
+ self.viewer.fov = settings.value('fov',22.5,float)
+ self.viewer.movement_speed = settings.value('movement_speed',10,float)
+ self.viewer.rotation_speed = settings.value('rotation_speed',1,float)
+ settings.endGroup()
+
+ def closeEvent(self,event):
+ #self.writeSettings()
+ super().closeEvent(event)
+
+ @QtCore.pyqtSlot(bool)
+ def on_undo_stack_cleanChanged(self,clean):
+ self.setWindowModified(not clean)
+
+ @QtCore.pyqtSlot()
+ def on_action_open_model_triggered(self):
+ file_name = QtGui.QFileDialog.getOpenFileName(
+ self,
+ 'Open Model',
+ self.windowFilePath(),
+ 'Nintendo J3D model (*.bmd *.bdl);;All files (*)')
+ if not file_name: return
+ self.load(file_name)
+
+ @QtCore.pyqtSlot()
+ def on_action_open_animation_triggered(self):
+ file_name = QtGui.QFileDialog.getOpenFileName(
+ self,
+ 'Open Animation',
+ os.path.dirname(self.windowFilePath()),
+ 'Nintendo J3D animation (*.bck *.btk *.btp *.bva *.brk *.bpk);;All files (*)')
+ if not file_name: return
+ self.loadAnimation(file_name)
+
+ @QtCore.pyqtSlot()
+ def on_action_save_model_triggered(self):
+ self.save(self.windowFilePath())
+
+ @QtCore.pyqtSlot()
+ def on_action_save_model_as_triggered(self):
+ file_name = QtGui.QFileDialog.getSaveFileName(
+ self,
+ 'Save Model',
+ self.windowFilePath(),
+ 'Nintendo J3D model (*.bmd *.bdl);;All files (*)')
+ if not file_name: return
+ self.save(file_name)
+
+ @QtCore.pyqtSlot(object)
+ def on_explorer_currentTextureChanged(self,texture):
+ self.preview.setTexture(texture)
+ self.texture.setTexture(texture)
+
+
+if __name__ == '__main__':
+ import sys
+ import argparse
+
+ def excepthook(*exception_info):
+ logger.error('unexpected error',exc_info=exception_info)
+
+ message = QtGui.QMessageBox(None)
+ message.setWindowTitle(QtGui.qApp.applicationName())
+ message.setIcon(QtGui.QMessageBox.Critical)
+ message.setText('An unexpected error occurred.')
+ message.setDetailedText(logging_stream.getvalue())
+ message.setStandardButtons(QtGui.QMessageBox.Ok)
+ message.setDefaultButton(QtGui.QMessageBox.Ok)
+ message.exec_()
+
+ QtGui.qApp.exit()
+
+ logger.info('Python version: %s',sys.version)
+ logger.info('NumPy version: %s',numpy.version.version)
+ logger.info('Qt version: %s',QtCore.QT_VERSION_STR)
+ logger.info('PyQt version: %s',QtCore.PYQT_VERSION_STR)
+ logger.info('PyOpenGL version: %s',OpenGL.__version__)
+
+ application = QtGui.QApplication(sys.argv)
+ application.setOrganizationName('BlankSoft')
+ application.setApplicationName('J3D View')
+
+ sys.excepthook = excepthook
+
+ parser = argparse.ArgumentParser(description='View Nintendo GameCube/Wii BMD/BDL files')
+ parser.add_argument('file_name',nargs='?',metavar='file',help='file to view')
+ #arguments = parser.parse_args(application.arguments()[1:]) #FIXME Doesn't work on Windows
+ arguments = parser.parse_args()
+
+ editor = Editor()
+ editor.show()
+
+ if arguments.file_name is not None:
+ editor.load(arguments.file_name)
+
+ application.exec_()
+
diff --git a/qt.py b/qt.py
new file mode 100644
index 0000000..a26867d
--- /dev/null
+++ b/qt.py
@@ -0,0 +1,191 @@
+from PyQt4 import QtCore,QtGui
+
+
+class ComboBox(QtGui.QComboBox):
+
+ def setItems(self,items):
+ self.items = items
+ self.clear()
+ self.addItems([item.name for item in items])
+ for i,item in enumerate(items):
+ self.setItemData(i,item)
+
+ def setValue(self,value):
+ self.setCurrentIndex(self.items.index(value))
+
+
+class PropertyUndoCommand(QtGui.QUndoCommand):
+
+ def __init__(self,property_owner,property_name,old_value,new_value):
+ super().__init__()
+ self.property_owner = property_owner
+ self.property_name = property_name
+ self.old_value = old_value
+ self.new_value = new_value
+ #FIXME: Set the text property to something?
+
+ def redo(self):
+ self.property_owner.setProperty(self.property_name,self.new_value)
+
+ def undo(self):
+ self.property_owner.setProperty(self.property_name,self.old_value)
+
+
+class PropertyWidget:
+
+ def __init__(self):
+ self.property_owner = None
+ self.property_name = None
+ self.property_changed_signal = None
+ self.undo_stack = None
+
+ def bindProperty(self,property_owner,property_name,property_changed_signal):
+ if self.property_changed_signal is not None:
+ self.property_changed_signal.disconnect(self.setValue)
+
+ self.property_owner = property_owner
+ self.property_name = property_name
+ self.setValue(self.property_owner.property(self.property_name))
+
+ self.property_changed_signal = property_changed_signal
+ self.property_changed_signal.connect(self.setValue)
+
+ def setUndoStack(self,undo_stack):
+ self.undo_stack = undo_stack
+
+ @QtCore.pyqtSlot(object)
+ def on_valueChanged(self,value):
+ if self.property_owner is None: return
+ old_value = self.property_owner.property(self.property_name)
+ if value == old_value: return
+ if self.undo_stack is not None:
+ self.undo_stack.push(PropertyUndoCommand(self.property_owner,self.property_name,old_value,value))
+ else:
+ self.property_owner.setProperty(self.property_name,value)
+
+
+class PropertyLineEdit(QtGui.QLineEdit,PropertyWidget):
+
+ def __init__(self,*args,**kwargs):
+ QtGui.QLineEdit.__init__(self,*args,**kwargs)
+ PropertyWidget.__init__(self)
+ self.editingFinished.connect(self.on_editingFinished)
+
+ @QtCore.pyqtSlot(str)
+ def setValue(self,value):
+ self.setText(value)
+
+ @QtCore.pyqtSlot()
+ def on_editingFinished(self):
+ self.on_valueChanged(self.text())
+
+
+class PropertyComboBox(ComboBox,PropertyWidget):
+
+ def __init__(self,*args,**kwargs):
+ ComboBox.__init__(self,*args,**kwargs)
+ PropertyWidget.__init__(self)
+ self.currentIndexChanged.connect(self.on_currentIndexChanged)
+
+ @QtCore.pyqtSlot(int)
+ def on_currentIndexChanged(self,index):
+ self.on_valueChanged(self.itemData(index))
+
+
+class PropertySpinBox(QtGui.QSpinBox,PropertyWidget):
+
+ def __init__(self,*args,**kwargs):
+ QtGui.QSpinBox.__init__(self,*args,**kwargs)
+ PropertyWidget.__init__(self)
+ self.setKeyboardTracking(False)
+ self.valueChanged.connect(self.on_valueChanged)
+
+
+class PropertyDoubleSpinBox(QtGui.QDoubleSpinBox,PropertyWidget):
+
+ def __init__(self,*args,**kwargs):
+ QtGui.QSpinBox.__init__(self,*args,**kwargs)
+ PropertyWidget.__init__(self)
+ self.setKeyboardTracking(False)
+ self.valueChanged.connect(self.on_valueChanged)
+
+
+class Property:
+
+ def __init__(self,property_type=object):
+ self.property_type = property_type
+
+ @staticmethod
+ def create_getter(property_name):
+ def getter(property_owner):
+ return getattr(property_owner,'_' + property_name)
+ return getter
+
+ @staticmethod
+ def create_setter(property_name):
+ def setter(property_owner,value):
+ setattr(property_owner,'_' + property_name,value)
+ return setter
+
+ def create_full_setter(self,property_name,notify_name):
+ internal_setter = self.create_setter(property_name)
+
+ def setter(property_owner,value):
+ try:
+ old_value = getattr(property_owner,property_name)
+ except AttributeError:
+ pass
+ else:
+ if value == old_value: return
+
+ internal_setter(property_owner,value)
+
+ getattr(property_owner,notify_name).emit(value)
+
+ return setter
+
+
+class PropertyOwnerMetaClass(QtCore.pyqtWrapperType):
+
+ def __new__(metacls,cls,bases,classdict):
+ property_items = [(key,value) for key,value in classdict.items() if isinstance(value,Property)]
+
+ for name,property_placeholder in property_items:
+ metacls.insert_property(classdict,name,property_placeholder)
+
+ return super().__new__(metacls,cls,bases,classdict)
+
+ @staticmethod
+ def insert_property(classdict,name,property_placeholder):
+ notify_name = name + '_changed'
+ notify = QtCore.pyqtSignal(property_placeholder.property_type)
+ classdict[notify_name] = notify
+
+ getter = property_placeholder.create_getter(name)
+ setter = property_placeholder.create_full_setter(name,notify_name)
+ classdict[name] = QtCore.pyqtProperty(property_placeholder.property_type,fget=getter,fset=setter,notify=notify)
+
+
+class Wrapper(QtCore.QObject,metaclass=PropertyOwnerMetaClass):
+
+ class Property(Property):
+
+ @staticmethod
+ def create_getter(property_name):
+ def getter(property_owner):
+ return getattr(property_owner.wrapped_object,property_name)
+ return getter
+
+ @staticmethod
+ def create_setter(property_name):
+ def setter(property_owner,value):
+ setattr(property_owner.wrapped_object,property_name,value)
+ return setter
+
+ def __init__(self,wrapped_object,*args,**kwargs):
+ super().__init__(*args,**kwargs)
+ self.wrapped_object = wrapped_object
+
+ def __getattr__(self,name):
+ return getattr(self.wrapped_object,name)
+
diff --git a/setup.py b/setup.py
new file mode 100644
index 0000000..32f9d07
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,39 @@
+#!/usr/bin/env python3
+
+try:
+ from cx_Freeze import setup,Executable
+ has_cx_freeze = True
+except ImportError:
+ from distutils.core import setup
+ has_cx_freeze = False
+ print('Could not import cx_Freeze. Building executable not possible.')
+
+import platform
+from distutils.extension import Extension
+from Cython.Build import cythonize
+import numpy
+
+arguments = dict(
+ name='J3D View',
+ version='0.3',
+ description='Nintendo GameCube/Wii BMD/BDL file viewer',
+ scripts = ['j3dview.py'],
+ py_modules=['gl','viewer_widget','explorer_widget','forms'],
+ packages=['btypes','gx','j3d'])
+
+arguments['ext_modules'] = cythonize(Extension(
+ 'gx.texture',
+ ['gx/texture.pyx'],
+ include_dirs=[numpy.get_include()]))
+
+if has_cx_freeze:
+ base = 'Win32GUI' if platform.system() == 'Windows' else None
+ build_exe = dict(
+ includes=['viewer_widget','explorer_widget','forms'],
+ packages=['OpenGL.platform','OpenGL.arrays'],
+ include_files=[('ui/Editor.ui','ui/Editor.ui'),('ui/ViewSettingsForm.ui','ui/ViewSettingsForm.ui'),('ui/TextureForm.ui','ui/TextureForm.ui')])
+ arguments['executables'] = [Executable('j3dview.py',base=base)]
+ arguments['options'] = dict(build_exe=build_exe)
+
+setup(**arguments)
+
diff --git a/ui/Editor.ui b/ui/Editor.ui
new file mode 100644
index 0000000..d8a1a2e
--- /dev/null
+++ b/ui/Editor.ui
@@ -0,0 +1,243 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 600
+
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+
+
+
+
+ Explorer
+
+
+ 1
+
+
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ false
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+ Preview
+
+
+ 1
+
+
+
+
+
+ View Settings
+
+
+ 2
+
+
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 77
+ 548
+
+
+
+
+
+
+
+
+
+
+ Texture
+
+
+ 1
+
+
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 254
+ 134
+
+
+
+
+
+
+
+
+
+
+ &Open Model...
+
+
+
+
+ Open Animation...
+
+
+
+
+ &Save Model
+
+
+
+
+ Save Model &As...
+
+
+
+
+ &Quit
+
+
+
+
+
+ ExplorerWidget
+ QTreeWidget
+
+
+
+ ViewerWidget
+ QWidget
+
+ 1
+
+
+ ViewSettingsForm
+ QWidget
+
+ 1
+
+
+ TextureForm
+ QWidget
+
+ 1
+
+
+
+
+
+ action_quit
+ triggered()
+ MainWindow
+ close()
+
+
+ -1
+ -1
+
+
+ 399
+ 299
+
+
+
+
+
diff --git a/ui/TextureForm.ui b/ui/TextureForm.ui
new file mode 100644
index 0000000..a7c46ce
--- /dev/null
+++ b/ui/TextureForm.ui
@@ -0,0 +1,262 @@
+
+
+ TextureForm
+
+
+
+ 0
+ 0
+ 246
+ 440
+
+
+
+ Form
+
+
+
+ QFormLayout::AllNonFixedFieldsGrow
+
+
+ 4
+
+
+ 0
+
+
+ 4
+
+ -
+
+
+ Name:
+
+
+
+ -
+
+
+ Image Format:
+
+
+
+ -
+
+
+ Image Size:
+
+
+
+ -
+
+
+ Image Levels:
+
+
+
+ -
+
+
+ Palette Format:
+
+
+
+ -
+
+
+ Palette Size:
+
+
+
+ -
+
+
+ Wrap S:
+
+
+
+ -
+
+
+ Wrap T:
+
+
+
+ -
+
+
+ Min. Filter:
+
+
+
+ -
+
+
+ Mag. Filter:
+
+
+
+ -
+
+
+ Min. LOD:
+
+
+
+ -
+
+
+ Max. LOD:
+
+
+
+ -
+
+
+ LOD Bias:
+
+
+
+ -
+
+
+ Unknown 0:
+
+
+
+ -
+
+
+ Unknown 1:
+
+
+
+ -
+
+
+ Unknown 2:
+
+
+
+ -
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ 10.000000000000000
+
+
+
+ -
+
+
+ 10.000000000000000
+
+
+
+ -
+
+
+ -4.000000000000000
+
+
+ 3.990000000000000
+
+
+
+ -
+
+
+ 255
+
+
+
+ -
+
+
+ 255
+
+
+
+ -
+
+
+ 255
+
+
+
+
+
+
+
+ PropertyLineEdit
+ QLineEdit
+
+
+
+ PropertyComboBox
+ QComboBox
+
+
+
+ PropertyDoubleSpinBox
+ QDoubleSpinBox
+
+
+
+ PropertySpinBox
+ QSpinBox
+
+
+
+
+
+
diff --git a/ui/ViewSettingsForm.ui b/ui/ViewSettingsForm.ui
new file mode 100644
index 0000000..8729da1
--- /dev/null
+++ b/ui/ViewSettingsForm.ui
@@ -0,0 +1,122 @@
+
+
+ ViewSettingsForm
+
+
+
+ 0
+ 0
+ 241
+ 143
+
+
+
+ Form
+
+
+
+ 4
+
+
+ 0
+
+
+ 4
+
+ -
+
+
+ Z Near:
+
+
+ z_near
+
+
+
+ -
+
+
+ 1000000.000000000000000
+
+
+
+ -
+
+
+ Z Far:
+
+
+ z_far
+
+
+
+ -
+
+
+ 1000000.000000000000000
+
+
+
+ -
+
+
+ Fov:
+
+
+ fov
+
+
+
+ -
+
+
+ 180.000000000000000
+
+
+
+ -
+
+
+ Movement Speed:
+
+
+ movement_speed
+
+
+
+ -
+
+
+ 1000000.000000000000000
+
+
+
+ -
+
+
+ Rotation Speed:
+
+
+ rotation_speed
+
+
+
+ -
+
+
+ 180.000000000000000
+
+
+
+
+
+
+
+ PropertyDoubleSpinBox
+ QDoubleSpinBox
+
+
+
+
+
+
diff --git a/viewer_widget.py b/viewer_widget.py
new file mode 100644
index 0000000..b245b8e
--- /dev/null
+++ b/viewer_widget.py
@@ -0,0 +1,256 @@
+from math import cos,sin,tan,radians
+import os.path
+import numpy
+from OpenGL.GL import *
+from PyQt4 import QtCore,QtGui,QtOpenGL,uic
+import qt
+from j3d.opengl import *
+
+import logging
+logger = logging.getLogger(__name__)
+
+
+class Quarternion:
+
+ def __init__(self,a=0,b=0,c=0,d=0):
+ self.a = a
+ self.b = b
+ self.c = c
+ self.d = d
+
+ def __mul__(self,other):
+ return Quarternion(
+ self.a*other.a - self.b*other.b - self.c*other.c - self.d*other.d,
+ self.a*other.b + self.b*other.a + self.c*other.d - self.d*other.c,
+ self.a*other.c - self.b*other.d + self.c*other.a + self.d*other.b,
+ self.a*other.d + self.b*other.c - self.c*other.b + self.d*other.a)
+
+ def conjugate(self):
+ return Quarternion(self.a,-self.b,-self.c,-self.d)
+
+ @staticmethod
+ def rotation(axis_x,axis_y,axis_z,angle):
+ s = sin(angle/2)
+ return Quarternion(cos(angle/2),s*axis_x,s*axis_y,s*axis_z)
+
+
+def create_rotation_matrix(rotation):
+ a,b,c,d = rotation.a,rotation.b,rotation.c,rotation.d
+ return numpy.array([
+ [a*a + b*b - c*c - d*d,2*(b*c - a*d),2*(b*d + a*c)],
+ [2*(b*c + a*d),a*a - b*b + c*c - d*d,2*(c*d - a*b)],
+ [2*(b*d - a*c),2*(c*d + a*b),a*a - b*b - c*c + d*d]],numpy.float32)
+
+
+def create_frustum_matrix(left,right,bottom,top,near,far):
+ return numpy.array([
+ [2*near/(right - left),0,(right + left)/(right - left),0],
+ [0,2*near/(top - bottom),(top + bottom)/(top - bottom),0],
+ [0,0,-(far + near)/(far - near),-2*far*near/(far - near)],
+ [0,0,-1,0]],
+ numpy.float32)
+
+
+class ViewerWidget(QtOpenGL.QGLWidget,metaclass=qt.PropertyOwnerMetaClass):
+
+ z_near = qt.Property(float)
+ z_far = qt.Property(float)
+ fov = qt.Property(float)
+ movement_speed = qt.Property(float)
+ rotation_speed = qt.Property(float)
+
+ @property
+ def projection_matrix(self):
+ return self.matrix_block['projection_matrix']
+
+ @projection_matrix.setter
+ def projection_matrix(self,matrix):
+ self.matrix_block['projection_matrix'] = matrix
+
+ @property
+ def view_matrix(self):
+ return self.matrix_block['view_matrix']
+
+ @view_matrix.setter
+ def view_matrix(self,matrix):
+ self.matrix_block['view_matrix'] = matrix
+
+ def __init__(self,parent=None,shareWidget=None,flags=QtCore.Qt.WindowFlags(0)):
+ display_format = QtOpenGL.QGLFormat()
+ display_format.setVersion(3,3)
+ display_format.setProfile(QtOpenGL.QGLFormat.CoreProfile)
+ display_format.setAlpha(True)
+ display_format.setStencil(False)
+ display_format.setSampleBuffers(True)
+ display_format.setSamples(4)
+
+ super().__init__(display_format,parent,shareWidget,flags)
+
+ self.setFocusPolicy(QtCore.Qt.StrongFocus)
+
+ self.model = None
+ self.animation = None
+
+ self.z_near = 25
+ self.z_far = 12800
+ self.fov = 22.5
+ self.view_position = numpy.array([0,0,1000],numpy.float32)
+ self.view_rotation = Quarternion(1,0,0,0)
+ self.movement_speed = 10
+ self.rotation_speed = 1
+ self.fps = 30
+
+ self.z_near_changed.connect(self.on_z_near_changed)
+ self.z_far_changed.connect(self.on_z_far_changed)
+ self.fov_changed.connect(self.on_fov_changed)
+
+ self.animation_timer = QtCore.QTimer(self,objectName='animation_timer')
+ self.animation_timer.timeout.connect(self.on_animation_timer_timeout)
+
+ self.pressed_keys = set()
+
+ QtGui.qApp.aboutToQuit.connect(self.on_application_aboutToQuit)
+
+ def update_projection_matrix(self):
+ u = self.z_near*tan(radians(self.fov))
+ r = u*self.width()/self.height()
+ self.projection_matrix = create_frustum_matrix(-r,r,-u,u,self.z_near,self.z_far)
+ self.projection_matrix_need_update = False
+
+ def update_view_matrix(self):
+ #FIXME: Renormalise rotation occasionally
+ self.view_matrix[:,0:3] = create_rotation_matrix(self.view_rotation)
+ self.view_matrix[:,3] = -numpy.dot(self.view_matrix[:,0:3],self.view_position)
+ self.view_matrix_need_update = False
+
+ def initializeGL(self):
+ logger.info('OpenGL vendor: %s',glGetString(GL_VENDOR).decode())
+ logger.info('OpenGL renderer: %s',glGetString(GL_RENDERER).decode())
+ logger.info('OpenGL version: %s',glGetString(GL_VERSION).decode())
+ logger.info('OpenGLSL version: %s',glGetString(GL_SHADING_LANGUAGE_VERSION).decode())
+
+ if self.format().sampleBuffers():
+ glEnable(GL_MULTISAMPLE)
+
+ self.matrix_block = MatrixBlock(GL_DYNAMIC_DRAW)
+ self.projection_matrix_need_update = True
+ self.view_matrix_need_update = True
+
+ self.animation_timer.start(1000/self.fps)
+
+ def paintGL(self):
+ glClearColor(0.5,0.5,0.5,1)
+ glClearDepth(1.0)
+ glDepthMask(True)
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
+ if self.model is not None:
+ self.matrix_block.bind(MATRIX_BLOCK_BINDING_POINT)
+ self.model.gl_draw()
+
+ def resizeGL(self,width,height):
+ glViewport(0,0,width,height)
+ self.projection_matrix_need_update = True
+
+ def sizeHint(self):
+ return QtCore.QSize(640,480)
+
+ @QtCore.pyqtSlot(float)
+ def on_z_near_changed(self,value):
+ self.projection_matrix_need_update = True
+
+ @QtCore.pyqtSlot(float)
+ def on_z_far_changed(self,value):
+ self.projection_matrix_need_update = True
+
+ @QtCore.pyqtSlot(float)
+ def on_fov_changed(self,value):
+ self.projection_matrix_need_update = True
+
+ @QtCore.pyqtSlot()
+ def on_animation_timer_timeout(self):
+ if QtGui.qApp.keyboardModifiers() & QtCore.Qt.ShiftModifier:
+ movement_speed = 5*self.movement_speed
+ rotation_speed = 5*self.rotation_speed
+ else:
+ movement_speed = self.movement_speed
+ rotation_speed = self.rotation_speed
+
+ if QtCore.Qt.Key_A in self.pressed_keys and QtCore.Qt.Key_D not in self.pressed_keys:
+ self.view_position -= movement_speed*self.view_matrix[0,0:3]
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_D in self.pressed_keys and QtCore.Qt.Key_A not in self.pressed_keys:
+ self.view_position += movement_speed*self.view_matrix[0,0:3]
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_Q in self.pressed_keys and QtCore.Qt.Key_E not in self.pressed_keys:
+ self.view_position -= movement_speed*self.view_matrix[1,0:3]
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_E in self.pressed_keys and QtCore.Qt.Key_Q not in self.pressed_keys:
+ self.view_position += movement_speed*self.view_matrix[1,0:3]
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_W in self.pressed_keys and QtCore.Qt.Key_S not in self.pressed_keys:
+ self.view_position -= movement_speed*self.view_matrix[2,0:3]
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_S in self.pressed_keys and QtCore.Qt.Key_W not in self.pressed_keys:
+ self.view_position += movement_speed*self.view_matrix[2,0:3]
+ self.view_matrix_need_update = True
+
+ if QtCore.Qt.Key_J in self.pressed_keys and QtCore.Qt.Key_L not in self.pressed_keys:
+ self.view_rotation = Quarternion.rotation(0,1,0,-radians(rotation_speed))*self.view_rotation
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_L in self.pressed_keys and QtCore.Qt.Key_J not in self.pressed_keys:
+ self.view_rotation = Quarternion.rotation(0,1,0,radians(rotation_speed))*self.view_rotation
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_I in self.pressed_keys and QtCore.Qt.Key_K not in self.pressed_keys:
+ self.view_rotation = Quarternion.rotation(1,0,0,-radians(rotation_speed))*self.view_rotation
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_K in self.pressed_keys and QtCore.Qt.Key_I not in self.pressed_keys:
+ self.view_rotation = Quarternion.rotation(1,0,0,radians(rotation_speed))*self.view_rotation
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_O in self.pressed_keys and QtCore.Qt.Key_U not in self.pressed_keys:
+ self.view_rotation = Quarternion.rotation(0,0,1,-radians(rotation_speed))*self.view_rotation
+ self.view_matrix_need_update = True
+ if QtCore.Qt.Key_U in self.pressed_keys and QtCore.Qt.Key_O not in self.pressed_keys:
+ self.view_rotation = Quarternion.rotation(0,0,1,radians(rotation_speed))*self.view_rotation
+ self.view_matrix_need_update = True
+
+ if self.projection_matrix_need_update:
+ self.update_projection_matrix()
+
+ if self.view_matrix_need_update:
+ self.update_view_matrix()
+
+ if self.animation is not None and not self.animation.is_finished:
+ self.animation.advance_frame()
+
+ self.updateGL()
+
+ def setModel(self,model):
+ self.makeCurrent()
+ model.gl_init()
+ self.model = model
+ self.animation = None
+
+ def setAnimation(self,animation):
+ animation.attach(self.model)
+ self.animation = animation
+
+ def keyPressEvent(self,event):
+ self.pressed_keys.add(event.key())
+ super().keyPressEvent(event)
+
+ def keyReleaseEvent(self,event):
+ self.pressed_keys.discard(event.key())
+ super().keyPressEvent(event)
+
+ def focusOutEvent(self,event):
+ self.pressed_keys = set()
+ super().focusOutEvent(event)
+
+ @QtCore.pyqtSlot()
+ def on_application_aboutToQuit(self):
+ #XXX Delete OpenGL objects before the OpenGL module starts to unload
+ #FIXME: Find a better way of doing this?
+ self.makeCurrent()
+ if hasattr(self,'matrix_block'): del self.matrix_block
+ if hasattr(self,'model'): del self.model
+