class NiBillboardNode(NiNode): # provide access to related enums BillboardMode = BillboardMode # flags access billboard_mode = enum_property(BillboardMode, mask=0x0060, pos=5)
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 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 NiMaterialColorController(NiTimeController): data: NiPosData | None = None # provide access to related enums ColorField = ColorField # convenience properties color_field = enum_property(ColorField, mask=0x0070, pos=4) _refs = (*NiTimeController._refs, "data") def load(self, stream): super().load(stream) self.data = stream.read_link() def save(self, stream): super().save(stream) stream.write_link(self.data)
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]