class NiZBufferProperty(NiProperty): # provide access to related enums TestFunction = TestFunction # flags access z_buffer_test = bool_property(mask=0x0001) z_buffer_write = bool_property(mask=0x0002) test_function = enum_property(TestFunction, mask=0x003C, pos=2) test_function_specified = bool_property(mask=0x0040)
class NiFltAnimationNode(NiSwitchNode): period: float32 = 0 # flags access bounce = bool_property(mask=0x0040) def load(self, stream): super().load(stream) self.period = stream.read_float() def save(self, stream): super().save(stream) stream.write_float(self.period)
class NiSwitchNode(NiNode): active_index: uint32 = 0 # flags access update_only_active = bool_property(mask=0x0020) def load(self, stream): super().load(stream) self.active_index = stream.read_uint() def save(self, stream): super().save(stream) stream.write_uint(self.active_index)
class NiAlphaProperty(NiProperty): test_ref: uint8 = 0 # [0, 255] # provide access to related enums AlphaBlendFunction = AlphaBlendFunction AlphaTestFunction = AlphaTestFunction # convenience properties alpha_blending = bool_property(mask=0x0001) src_blend_mode = enum_property(AlphaBlendFunction, mask=0x001E, pos=1) dst_blend_mode = enum_property(AlphaBlendFunction, mask=0x01E0, pos=5) alpha_testing = bool_property(mask=0x0200) test_mode = enum_property(AlphaTestFunction, mask=0x1C00, pos=10) no_sort = bool_property(mask=0x2000) def load(self, stream): super().load(stream) self.test_ref = stream.read_ubyte() def save(self, stream): super().save(stream) stream.write_ubyte(self.test_ref)
class NiLookAtController(NiTimeController): look_at: NiAVObject | None = None _ptrs = (*NiTimeController._ptrs, "look_at") # provide access to related enums Axis = Axis # convenience properties flip = bool_property(mask=0x0010) axis = enum_property(Axis, mask=0x0060, pos=5) def load(self, stream): super().load(stream) self.look_at = stream.read_link() def save(self, stream): super().save(stream) stream.write_link(self.look_at)
class NiTimeController(NiObject): next: NiTimeController | None = None flags: uint16 = 8 frequency: float32 = 1.0 phase: float32 = 0.0 start_time: float32 = 0.0 stop_time: float32 = 0.0 target: NiObjectNET | None = None # provide access to related enums CycleType = CycleType # flags access cycle_type = enum_property(CycleType, mask=0x0006, pos=1) active = bool_property(mask=0x0008) _refs = (*NiObject._refs, "next") _ptrs = (*NiObject._ptrs, "target") def load(self, stream): self.next = stream.read_link() self.flags = stream.read_ushort() self.frequency = stream.read_float() self.phase = stream.read_float() self.start_time = stream.read_float() self.stop_time = stream.read_float() self.target = stream.read_link() def save(self, stream): stream.write_link(self.next) stream.write_ushort(self.flags) stream.write_float(self.frequency) stream.write_float(self.phase) stream.write_float(self.start_time) stream.write_float(self.stop_time) stream.write_link(self.target) def update_start_stop_times(self) -> tuple[int, int]: if self.data: self.start_time, self.stop_time = self.data.get_start_stop_times() else: self.start_time, self.stop_time = 0, 0
class NiAVObject(NiObjectNET): flags: uint16 = 0 translation: NiPoint3 = ZERO3 rotation: NiMatrix3 = ID33 scale: float32 = 1.0 velocity: NiPoint3 = ZERO3 properties: list[NiProperty | None] = [] bounding_volume: NiBoundingVolume | None = None # TODO: remove children = [] # type: list[NiAVObject | None] # provide access to related enums PropagateMode = PropagateMode # flags access app_culled = bool_property(mask=0x0001) propagate_mode = enum_property(PropagateMode, mask=0x0006, pos=1) visual = bool_property(mask=0x0008) _refs = (*NiObjectNET._refs, "properties") def load(self, stream): super().load(stream) self.flags = stream.read_ushort() self.translation = stream.read_floats(3) self.rotation = stream.read_floats(3, 3) self.scale = stream.read_float() self.velocity = stream.read_floats(3) self.properties = stream.read_links() has_bounding_volume = stream.read_bool() if has_bounding_volume: self.bounding_volume = NiBoundingVolume.load(stream) def save(self, stream): super().save(stream) stream.write_ushort(self.flags) stream.write_floats(self.translation) stream.write_floats(self.rotation) stream.write_float(self.scale) stream.write_floats(self.velocity) stream.write_links(self.properties) stream.write_bool(self.bounding_volume) if self.bounding_volume: self.bounding_volume.save(stream) def sort(self, key=lambda prop: prop.type): super().sort() self.properties.sort(key=key) def apply_scale(self, scale: float): self.translation *= scale if self.bounding_volume: self.bounding_volume.apply_scale(scale) def get_property(self, property_type: type[T]) -> T: for prop in self.properties: if isinstance(prop, property_type): return prop @property def matrix(self) -> ndarray: return compose(self.translation, self.rotation, self.scale) @matrix.setter def matrix(self, value: ndarray): self.translation, self.rotation, self.scale = decompose_uniform(value) def matrix_relative_to(self, ancestor: NiAVObject) -> ndarray: path = reversed(list(self.find_path(ancestor))) return dotproduct([obj.matrix for obj in path]) @property def is_biped(self) -> bool: return self.name.lower().startswith("bip01") @property def is_shadow(self) -> bool: return self.name.lower().startswith(("shadow", "tri shadow")) @property def is_bounding_box(self) -> bool: return bool(self.bounding_volume) and self.name.lower().startswith( "bounding box") def descendants(self, breadth_first=False) -> Iterator[NiAVObject]: queue = deque(filter(None, self.children)) extend, iterator = (queue.extendleft, iter) if breadth_first else \ (queue.extend, reversed) while queue: node = queue.pop() yield node extend(child for child in iterator(node.children) if child) def descendants_pairs( self, breadth_first=False) -> Iterator[tuple[NiAVObject, NiAVObject]]: """Similar to descendants, but yielding pairs of (parent, node).""" queue = deque((self, child) for child in self.children if child) extend, iterator = (queue.extendleft, iter) if breadth_first else \ (queue.extend, reversed) while queue: parent, node = queue.pop() yield parent, node extend((node, child) for child in iterator(node.children) if child) def find_path(self, ancestor, breadth_first=True) -> Iterator[NiAVObject]: parents = {} for parent, node in ancestor.descendants_pairs(breadth_first): parents[node] = parent if node is self: break else: raise ValueError( f"find_path: no path from {self} to {ancestor} exists") while node is not ancestor: yield node node = parents[node]
class NiBSAnimationNode(NiNode): # flags access animated = bool_property(mask=0x0020) not_random = bool_property(mask=0x0040)
class NiBSParticleNode(NiBSAnimationNode): # flags access follow = bool_property(mask=0x0080)
class NiWireframeProperty(NiProperty): wireframe = bool_property(mask=0x0001)
class NiCollisionSwitch(NiNode): # flags access collidable = bool_property(mask=0x0020)
class NiGeometry(NiAVObject): data: NiGeometryData | None = None skin: NiSkinInstance | None = None # flags access compress_vertices = bool_property(mask=0x0008) compress_normals = bool_property(mask=0x0010) compress_uv_sets = bool_property(mask=0x0020) shadow = bool_property(mask=0x0040) _refs = (*NiAVObject._refs, "data", "skin") def load(self, stream): super().load(stream) self.data = stream.read_link() self.skin = stream.read_link() def save(self, stream): super().save(stream) stream.write_link(self.data) stream.write_link(self.skin) @property def bone_influences(self): try: skin = self.skin.root and self.skin assert len(skin.bones) == len(skin.data.bone_data) except (AttributeError, AssertionError): return () return tuple(zip(skin.bones, skin.data.bone_data)) @property def morph_targets(self): try: basis, *targets = self.controller.data.targets assert len(basis.vertices) == len(self.data.vertices) except (AttributeError, AssertionError, ValueError): return () return tuple(targets) def vertex_weights(self): bone_influences = self.bone_influences vertex_weights = zeros(len(bone_influences), len(self.data.vertices)) for i, (_, bone_data) in enumerate(bone_influences): indices = bone_data.vertex_weights["f0"] weights = bone_data.vertex_weights["f1"] vertex_weights[i, indices] = weights return vertex_weights def vertex_morphs(self): morph_targets = self.morph_targets vertex_morphs = zeros(len(morph_targets), len(self.data.vertices), 3) for i, target in enumerate(morph_targets): vertex_morphs[ i] = self.data.vertices + target.vertices # TODO not always relative! return vertex_morphs def apply_skin(self, keep_skin=False): data = self.data skin = self.skin if not keep_skin: self.skin = None deformed_verts = zeros(*data.vertices.shape) deformed_norms = zeros(*data.normals.shape) root_to_skin = skin.data.matrix for bone, bone_data in zip(skin.bones, skin.data.bone_data): skin_to_bone = bone_data.matrix bone_matrix = bone.matrix_relative_to(skin.root) bind_matrix = root_to_skin @ bone_matrix @ skin_to_bone location, rotation, scale = decompose_uniform(bind_matrix) # indices and weights i = bone_data.vertex_weights["f0"] w = bone_data.vertex_weights["f1"][:, None] if len(deformed_verts): deformed_verts[i] += w * ( data.vertices[i] @ rotation.T * scale + location.T) if len(deformed_norms): deformed_norms[i] += w * (data.normals[i] @ rotation.T) data.vertices = deformed_verts data.normals = deformed_norms # TODO: normalize?