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 + + + + + + + + + + 0 + 0 + 800 + 25 + + + + + &File + + + + + + + + + + + + &Edit + + + + + &Window + + + + + + + + + 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 +
explorer_widget
+
+ + ViewerWidget + QWidget +
viewer_widget
+ 1 +
+ + ViewSettingsForm + QWidget +
forms
+ 1 +
+ + TextureForm + QWidget +
forms
+ 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 +
qt
+
+ + PropertyComboBox + QComboBox +
qt
+
+ + PropertyDoubleSpinBox + QDoubleSpinBox +
qt
+
+ + PropertySpinBox + QSpinBox +
qt
+
+
+ + +
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 +
qt
+
+
+ + +
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 +