Files
BallanceBlenderHelper/bbp_ng/UTIL_blender_mesh.py

548 lines
22 KiB
Python
Raw Normal View History

2023-10-20 10:40:20 +08:00
import bpy, bmesh, mathutils
2023-10-17 11:50:31 +08:00
import typing, array, collections
from . import UTIL_functions, UTIL_virtools_types
2023-10-16 10:12:05 +08:00
## Blender Mesh Usage
# This module create a universal mesh visitor, including MeshReader, MeshWriter and MeshUVModifier
# for every other possible module using.
# Obviously, MeshReader is served for 2 exporter, MeshWriter is served for 2 importer.
# MeshWriter also served for BMERevenge module and Ballance element loading.
# MeshUVModifier is used by Flatten UV and Rail UV.
2023-10-17 11:50:31 +08:00
#
#region Assist Functions
2023-10-18 12:09:40 +08:00
class FaceVertexData():
mPosIdx: int
mNmlIdx: int
mUvIdx: int
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
def __init__(self, pos: int = 0, nml: int = 0, uv: int = 0):
self.mPosIdx = pos
self.mNmlIdx = nml
self.mUvIdx = uv
class FaceData():
2023-10-19 10:56:33 +08:00
## @remark List or tuple. List is convenient for adding and removing
2023-12-01 23:27:53 +08:00
mIndices: tuple[FaceVertexData, ...] | list[FaceVertexData]
2023-10-19 10:56:33 +08:00
## Face used material slot index
# @remark If material slot is empty, or this face do not use material, set this value to 0.
2023-10-18 12:09:40 +08:00
mMtlIdx: int
2023-10-19 10:56:33 +08:00
2023-12-01 23:27:53 +08:00
def __init__(self, indices: tuple[FaceVertexData, ...] | list[FaceVertexData] = tuple(), mtlidx: int = 0):
2023-10-18 12:09:40 +08:00
self.mIndices = indices
self.mMtlIdx = mtlidx
2023-11-14 22:16:12 +08:00
2023-11-10 12:26:04 +08:00
def conv_co(self) -> None:
"""
Change indice order between Virtools and Blender
"""
if isinstance(self.mIndices, list):
self.mIndices.reverse()
elif isinstance(self.mIndices, tuple):
self.mIndices = self.mIndices[::-1]
else:
raise UTIL_functions.BBPException('invalid indices container.')
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
def is_indices_legal(self) -> bool:
return len(self.mIndices) >= 3
2023-11-10 12:26:04 +08:00
class MeshWriterIngredient():
mVertexPosition: typing.Iterator[UTIL_virtools_types.VxVector3] | None
mVertexNormal: typing.Iterator[UTIL_virtools_types.VxVector3] | None
mVertexUV: typing.Iterator[UTIL_virtools_types.VxVector2] | None
mFace: typing.Iterator[FaceData] | None
2023-11-11 13:32:58 +08:00
mMaterial: typing.Iterator[bpy.types.Material | None] | None
2023-11-10 12:26:04 +08:00
def __init__(self):
self.mVertexPosition = None
self.mVertexNormal = None
self.mVertexUV = None
self.mFace = None
self.mMaterial = None
def is_valid(self) -> bool:
if self.mVertexPosition is None: return False
if self.mVertexNormal is None: return False
if self.mVertexUV is None: return False
if self.mFace is None: return False
if self.mMaterial is None: return False
return True
def _flat_vxvector3(it: typing.Iterator[UTIL_virtools_types.VxVector3]) -> typing.Iterator[float]:
2023-10-17 11:50:31 +08:00
for entry in it:
2023-10-18 12:09:40 +08:00
yield entry.x
yield entry.y
yield entry.z
2023-10-17 11:50:31 +08:00
def _flat_vxvector2(it: typing.Iterator[UTIL_virtools_types.VxVector2]) -> typing.Iterator[float]:
2023-10-17 11:50:31 +08:00
for entry in it:
2023-10-18 12:09:40 +08:00
yield entry.x
yield entry.y
def _flat_face_nml_index(nml_idx: array.array, nml_array: array.array) -> typing.Iterator[float]:
2023-10-18 12:09:40 +08:00
for idx in nml_idx:
pos: int = idx * 3
yield nml_array[pos]
yield nml_array[pos + 1]
yield nml_array[pos + 2]
def _flat_face_uv_index(uv_idx: array.array, uv_array: array.array) -> typing.Iterator[float]:
2023-10-18 12:09:40 +08:00
for idx in uv_idx:
pos: int = idx * 2
yield uv_array[pos]
yield uv_array[pos + 1]
2023-11-14 22:16:12 +08:00
def _nest_custom_split_normal(nml_array: array.array) -> typing.Iterator[UTIL_virtools_types.ConstVxVector3]:
# following statement create a iterator for normals array by `iter(nml_array)`
# then triple it, because iterator is a reference type, so 3 items of this tuple is pointed to the same iterator and share the same iteration progress.
# then use star macro to pass it to zip, it will cause zip receive 3 params pointing to the same iterator.
# now zip() will call 3 params __next__() function from left to right.
# zip will get following iteration result because all iterator are the same one: (0, 1, 2), (3, 4, 5) and etc (number is index to corresponding value).
# finally, use tuple to expand it to a tuple, not a generator.
return tuple(zip(*(iter(nml_array), ) * 3))
2023-10-17 11:50:31 +08:00
2023-10-19 10:56:33 +08:00
class TemporaryMesh():
"""
Create a temporary mesh for convenient exporting.
When exporting mesh, we need evaluate it first, then triangulate it.
We create a temporary mesh to hold the evaluated and triangulated mesh result.
So that original object will not be affected and keep its original geometry.
Please note passed bpy.types.Object must be an object which can be converted into mesh.
You can use this class provided static method to check it.
2023-10-19 10:56:33 +08:00
"""
__cAllowedObjectType: typing.ClassVar[set[str]] = {
'MESH', 'CURVE', 'SURFACE', 'FONT', 'META'
}
@staticmethod
def has_geometry(obj: bpy.types.Object):
"""
Check whether given Blender object has geometry.
If it has, it can safely utilize this class for visiting mesh.
"""
return obj.type in TemporaryMesh.__cAllowedObjectType
2023-10-19 10:56:33 +08:00
__mBindingObject: bpy.types.Object
__mEvaluatedObject: bpy.types.Object
__mTempMesh: bpy.types.Mesh
2023-10-19 10:56:33 +08:00
def __init__(self, binding_obj: bpy.types.Object):
depsgraph = bpy.context.evaluated_depsgraph_get()
2023-10-19 10:56:33 +08:00
self.__mBindingObject = binding_obj
self.__mEvaluatedObject = self.__mBindingObject.evaluated_get(depsgraph)
self.__mTempMesh = self.__mEvaluatedObject.to_mesh()
2023-10-19 10:56:33 +08:00
if self.__mTempMesh is None:
2023-10-19 10:56:33 +08:00
raise UTIL_functions.BBPException('try getting mesh from an object without mesh.')
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.dispose()
def is_valid(self) -> bool:
if self.__mBindingObject is None: return False
2023-10-19 10:56:33 +08:00
if self.__mBindingObject is None: return False
if self.__mTempMesh is None: return False
return True
2023-10-20 10:40:20 +08:00
def dispose(self) -> None:
2023-10-19 10:56:33 +08:00
if self.is_valid():
self.__mTempMesh = None
self.__mEvaluatedObject.to_mesh_clear()
self.__mEvaluatedObject = None
2023-10-19 10:56:33 +08:00
self.__mBindingObject = None
def get_temp_mesh(self) -> bpy.types.Mesh:
if not self.is_valid():
raise UTIL_functions.BBPException('try calling invalid TemporaryMesh.')
return self.__mTempMesh
2023-10-17 11:50:31 +08:00
#endregion
2023-10-16 10:12:05 +08:00
class MeshReader():
2023-10-17 11:50:31 +08:00
"""
The passed mesh must be created by bpy.types.Object.to_mesh() and destroyed by bpy.types.Object.to_mesh_clear().
Because this class must trianglate mesh. To prevent change original mesh, this operations is essential.
2023-10-19 10:56:33 +08:00
A helper class TemporaryMesh can help you do this.
2023-10-17 11:50:31 +08:00
"""
2023-10-19 10:56:33 +08:00
__mAssocMesh: bpy.types.Mesh ##< The binding mesh for this reader. None if this reader is invalid.
2023-10-19 10:56:33 +08:00
def __init__(self, assoc_mesh: bpy.types.Mesh):
self.__mAssocMesh = assoc_mesh
# triangulate temp mesh
if self.is_valid():
self.__triangulate_mesh()
def is_valid(self) -> bool:
return self.__mAssocMesh is not None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.dispose()
2023-10-20 10:40:20 +08:00
def dispose(self) -> None:
2023-10-19 10:56:33 +08:00
if self.is_valid():
# reset mesh
self.__mAssocMesh = None
def get_vertex_position_count(self) -> int:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
return len(self.__mAssocMesh.vertices)
def get_vertex_position(self) -> typing.Iterator[UTIL_virtools_types.VxVector3]:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
cache: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3()
for vec in self.__mAssocMesh.vertices:
cache.x = vec.co.x
cache.y = vec.co.y
cache.z = vec.co.z
yield cache
def get_vertex_normal_count(self) -> int:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
# return loops count, equaling with face count * 3 in theory.
return len(self.__mAssocMesh.loops)
def get_vertex_normal(self) -> typing.Iterator[UTIL_virtools_types.VxVector3]:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
cache: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3()
for nml in self.__mAssocMesh.corner_normals:
cache.x = nml.vector.x
cache.y = nml.vector.y
cache.z = nml.vector.z
2023-10-19 10:56:33 +08:00
yield cache
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
def get_vertex_uv_count(self) -> int:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
if self.__mAssocMesh.uv_layers.active is None:
# if no uv layer, we need make a fake one
# return the same value with normals.
# it also mean create uv for each face vertex
return len(self.__mAssocMesh.loops)
else:
# otherwise return its size, also equaling with face count * 3 in theory
return len(self.__mAssocMesh.uv_layers.active.uv)
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
def get_vertex_uv(self) -> typing.Iterator[UTIL_virtools_types.VxVector2]:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
cache: UTIL_virtools_types.VxVector2 = UTIL_virtools_types.VxVector2()
if self.__mAssocMesh.uv_layers.active is None:
# create a fake one
cache.x = 0.0
cache.y = 0.0
for _ in range(self.get_vertex_uv_count()):
yield cache
else:
for uv in self.__mAssocMesh.uv_layers.active.uv:
cache.x = uv.vector.x
cache.y = uv.vector.y
yield cache
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
def get_material_slot_count(self) -> int:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
return len(self.__mAssocMesh.materials)
2023-11-14 22:16:12 +08:00
2023-11-11 13:32:58 +08:00
def get_material_slot(self) -> typing.Iterator[bpy.types.Material | None]:
2023-10-19 10:56:33 +08:00
"""
@remark This generator may return None if this slot do not link to may material.
"""
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
for mtl in self.__mAssocMesh.materials:
yield mtl
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
def get_face_count(self) -> int:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
return len(self.__mAssocMesh.polygons)
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
def get_face(self) -> typing.Iterator[FaceData]:
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshReader.')
# detect whether we have material
no_mtl: bool = self.get_material_slot_count() == 0
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
# use list as indices container for convenient adding and deleting.
cache: FaceData = FaceData([], 0)
for face in self.__mAssocMesh.polygons:
# confirm material use
# a face without mtl have 2 situations. first is the whole object do not have mtl
# another is this face use an empty mtl slot.
if no_mtl:
cache.mMtlIdx = 0
else:
cache.mMtlIdx = face.material_index
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
# resize indices
self.__resize_face_data_indices(cache.mIndices, face.loop_total)
# set indices
for i in range(face.loop_total):
cache.mIndices[i].mPosIdx = self.__mAssocMesh.loops[face.loop_start + i].vertex_index
cache.mIndices[i].mNmlIdx = face.loop_start + i
cache.mIndices[i].mUvIdx = face.loop_start + i
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
# return value
yield cache
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
def __resize_face_data_indices(self, ls: list[FaceVertexData], expected_size: int) -> None:
diff: int = expected_size - len(ls)
if diff > 0:
# add entry
for _ in range(diff):
ls.append(FaceVertexData())
elif diff < 0:
# remove entry
for _ in range(diff):
ls.pop()
else:
# no count diff, pass
pass
2023-11-14 22:16:12 +08:00
2023-10-19 10:56:33 +08:00
def __triangulate_mesh(self) -> None:
2023-11-10 12:26:04 +08:00
bm: bmesh.types.BMesh = bmesh.new()
2023-10-19 10:56:33 +08:00
bm.from_mesh(self.__mAssocMesh)
bmesh.ops.triangulate(bm, faces = bm.faces)
bm.to_mesh(self.__mAssocMesh)
bm.free()
2023-10-16 10:12:05 +08:00
class MeshWriter():
2023-10-17 11:50:31 +08:00
"""
2023-10-18 12:09:40 +08:00
If face do not use material, pass 0 as its material index.
If face do not have UV becuase it doesn't have material, you at least create 1 UV vector, eg. (0, 0),
then refer it to all face uv.
2023-10-17 11:50:31 +08:00
"""
__mAssocMesh: bpy.types.Mesh ##< The binding mesh for this writer. None if this writer is invalid.
2023-10-17 11:50:31 +08:00
__mVertexPos: array.array ##< Array item is float(f). Length must be an integer multiple of 3.
__mVertexNormal: array.array ##< Array item is float(f). Length must be an integer multiple of 3.
__mVertexUV: array.array ##< Array item is float(f). Length must be an integer multiple of 2.
## Array item is int32(L).
2023-10-18 12:09:40 +08:00
# Length must be the sum of each items in __mFaceVertexCount.
# Item is face vertex position index, based on 0, pointing to __mVertexPos (visiting need multiple it with 3 because __mVertexPos is flat struct).
__mFacePosIndices: array.array
## Same as __mFacePosIndices, but store face vertex normal index.
# Array item is int32(L). Length is equal to __mFacePosIndices
__mFaceNmlIndices: array.array
## Same as __mFacePosIndices, but store face vertex uv index.
# Array item is int32(L). Length is equal to __mFacePosIndices
__mFaceUvIndices: array.array
2023-10-19 10:56:33 +08:00
## Array item is int32(L).
2023-10-17 11:50:31 +08:00
# Length is the face count.
2023-10-18 12:09:40 +08:00
# It indicate how much vertex need to be consumed in __mFacePosIndices, __mFaceNmlIndices and __mFaceUvIndices for one face.
2023-10-17 11:50:31 +08:00
__mFaceVertexCount: array.array
__mFaceMtlIdx: array.array ##< Array item is int32(L). Length is equal to __mFaceVertexCount.
2023-10-18 12:09:40 +08:00
## Material Slot.
# Each item is unique make sure by __mMtlSlotMap
2023-11-11 13:32:58 +08:00
__mMtlSlot: list[bpy.types.Material | None]
2023-10-18 12:09:40 +08:00
## The map to make sure every item in __mMtlSlot is unique.
2023-10-19 10:56:33 +08:00
# Key is bpy.types.Material
2023-10-18 12:09:40 +08:00
# Value is key's index in __mMtlSlot.
2023-11-11 13:32:58 +08:00
__mMtlSlotMap: dict[bpy.types.Material | None, int]
2023-10-17 11:50:31 +08:00
## The attribute name storing temporary normals data inside mesh.
__cTempNormalAttrName: typing.ClassVar[str] = 'temp_custom_normals'
2023-10-17 11:50:31 +08:00
def __init__(self, assoc_mesh: bpy.types.Mesh):
self.__mAssocMesh = assoc_mesh
self.__mVertexPos = array.array('f')
self.__mVertexNormal = array.array('f')
self.__mVertexUV = array.array('f')
2023-10-18 12:09:40 +08:00
self.__mFacePosIndices = array.array('L')
self.__mFaceNmlIndices = array.array('L')
self.__mFaceUvIndices = array.array('L')
2023-10-17 11:50:31 +08:00
self.__mFaceVertexCount = array.array('L')
self.__mFaceMtlIdx = array.array('L')
self.__mMtlSlot = []
self.__mMtlSlotMap = {}
def is_valid(self) -> bool:
return self.__mAssocMesh is not None
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.dispose()
2023-10-19 10:56:33 +08:00
def dispose(self):
if self.is_valid():
# write mesh
self.__write_mesh()
# reset mesh
self.__mAssocMesh = None
2023-11-10 12:26:04 +08:00
def add_ingredient(self, data: MeshWriterIngredient):
2023-10-19 10:56:33 +08:00
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshWriter.')
2023-10-17 11:50:31 +08:00
if not data.is_valid():
raise UTIL_functions.BBPException('invalid mesh part data.')
# add vertex data
2023-11-10 12:26:04 +08:00
prev_vertex_pos_count: int = len(self.__mVertexPos) // 3
self.__mVertexPos.extend(_flat_vxvector3(data.mVertexPosition))
2023-11-10 12:26:04 +08:00
prev_vertex_nml_count: int = len(self.__mVertexNormal) // 3
self.__mVertexNormal.extend(_flat_vxvector3(data.mVertexNormal))
2023-11-10 12:26:04 +08:00
prev_vertex_uv_count: int = len(self.__mVertexUV) // 2
self.__mVertexUV.extend(_flat_vxvector2(data.mVertexUV))
2023-10-17 11:50:31 +08:00
# add material slot data and create mtl remap
mtl_remap: list[int] = []
for mtl in data.mMaterial:
idx: int | None = self.__mMtlSlotMap.get(mtl, None)
2023-11-10 12:26:04 +08:00
if idx is not None:
2023-10-17 11:50:31 +08:00
mtl_remap.append(idx)
else:
self.__mMtlSlotMap[mtl] = len(self.__mMtlSlot)
mtl_remap.append(len(self.__mMtlSlot))
self.__mMtlSlot.append(mtl)
# add face data
for face in data.mFace:
# check indices count
2023-10-18 12:09:40 +08:00
if not face.is_indices_legal():
2023-10-17 11:50:31 +08:00
raise UTIL_functions.BBPException('face must have at least 3 vertex.')
# add indices
2023-10-18 12:09:40 +08:00
for vec_index in face.mIndices:
self.__mFacePosIndices.append(vec_index.mPosIdx + prev_vertex_pos_count)
self.__mFaceNmlIndices.append(vec_index.mNmlIdx + prev_vertex_nml_count)
self.__mFaceUvIndices.append(vec_index.mUvIdx + prev_vertex_uv_count)
self.__mFaceVertexCount.append(len(face.mIndices))
2023-10-17 11:50:31 +08:00
# add face mtl with remap
2023-10-18 12:09:40 +08:00
mtl_idx: int = face.mMtlIdx
2023-11-10 12:26:04 +08:00
if mtl_idx < 0 or mtl_idx >= len(mtl_remap):
2023-10-18 12:09:40 +08:00
# fall back. add 0
self.__mFaceMtlIdx.append(0)
else:
self.__mFaceMtlIdx.append(mtl_remap[mtl_idx])
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
def __write_mesh(self):
# detect status
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshWriter.')
# and clear mesh
self.__clear_mesh()
# push material data
for mtl in self.__mMtlSlot:
self.__mAssocMesh.materials.append(mtl)
2023-11-14 22:16:12 +08:00
2023-10-18 12:09:40 +08:00
# add corresponding count for vertex position
2023-11-10 12:26:04 +08:00
self.__mAssocMesh.vertices.add(len(self.__mVertexPos) // 3)
2023-10-18 12:09:40 +08:00
# add loops data, it is the sum count of indices
# we use face pos indices size to get it
self.__mAssocMesh.loops.add(len(self.__mFacePosIndices))
# set face count
self.__mAssocMesh.polygons.add(len(self.__mFaceVertexCount))
# create uv layer
self.__mAssocMesh.uv_layers.new(do_init = False)
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
# add vertex position data
self.__mAssocMesh.vertices.foreach_set('co', self.__mVertexPos)
# add face vertex pos index data
self.__mAssocMesh.loops.foreach_set('vertex_index', self.__mFacePosIndices)
# add face vertex nml by function via mesh custom attribute
# NOTE: Blender 4.0 / 4.1 changed. I copy these code from FBX Importer.
temp_normal_attribute: bpy.types.FloatVectorAttribute
temp_normal_attribute = typing.cast(
bpy.types.FloatVectorAttribute,
self.__mAssocMesh.attributes.new(MeshWriter.__cTempNormalAttrName, 'FLOAT_VECTOR', 'CORNER')
)
temp_normal_attribute.data.foreach_set('vector',
2023-11-14 22:16:12 +08:00
tuple(_flat_face_nml_index(self.__mFaceNmlIndices, self.__mVertexNormal))
2023-10-18 12:09:40 +08:00
)
# add face vertex uv by function
self.__mAssocMesh.uv_layers.active.uv.foreach_set('vector',
2023-11-14 22:16:12 +08:00
tuple(_flat_face_uv_index(self.__mFaceUvIndices, self.__mVertexUV))
2023-10-18 12:09:40 +08:00
) # NOTE: blender 3.5 changed. UV must be visited by .uv, not the .data
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
# iterate face to set face data
2023-11-14 22:16:12 +08:00
f_vertex_idx: int = 0
2023-10-18 12:09:40 +08:00
for fi in range(len(self.__mFaceVertexCount)):
# set start loop
# NOTE: blender 3.6 changed. Loop setting in polygon do not need set loop_total any more.
# the loop_total will be auto calculated by the next loop_start.
# loop_total become read-only
2023-11-14 22:16:12 +08:00
self.__mAssocMesh.polygons[fi].loop_start = f_vertex_idx
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
# set material index
self.__mAssocMesh.polygons[fi].material_index = self.__mFaceMtlIdx[fi]
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
# set auto smooth. it is IMPORTANT
# because it related to whether custom split normal can work
self.__mAssocMesh.polygons[fi].use_smooth = True
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
# inc vertex idx
2023-11-14 22:16:12 +08:00
f_vertex_idx += self.__mFaceVertexCount[fi]
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
# validate mesh.
# it is IMPORTANT that do NOT delete custom data
# because we need use these data to set custom split normal later
2023-11-14 21:37:29 +08:00
self.__mAssocMesh.validate(clean_customdata = False)
2023-10-18 12:09:40 +08:00
# update mesh without mesh calc
self.__mAssocMesh.update(calc_edges = False, calc_edges_loose = False)
2023-10-19 10:56:33 +08:00
2023-10-18 12:09:40 +08:00
# set custom split normal data
2023-11-14 22:16:12 +08:00
# this operation must copy preserved normal data from loops, not the array data in this class,
# because the validate() may change the mesh and if change happended, an error will occur when applying normals (not matched loops count).
# this should not happend in normal case, for testing, please load "Level_1.NMO" (Ballance Level 1).
# copy data from loops preserved in validate().
# NOTE: Blender 4.0 / 4.1 changed. I copy these code from FBX Importer.
2023-11-14 22:16:12 +08:00
loops_normals = array.array('f', [0.0] * (len(self.__mAssocMesh.loops) * 3))
temp_normal_attribute = typing.cast(
bpy.types.FloatVectorAttribute,
self.__mAssocMesh.attributes[MeshWriter.__cTempNormalAttrName]
)
temp_normal_attribute.data.foreach_get("vector", loops_normals)
2023-11-14 22:16:12 +08:00
# apply data
self.__mAssocMesh.normals_split_custom_set(
tuple(_nest_custom_split_normal(loops_normals))
)
self.__mAssocMesh.attributes.remove(
# MARK: idk why I need fucking get this attribute again.
# But if I were not, this function must raise bullshit exception!
self.__mAssocMesh.attributes[MeshWriter.__cTempNormalAttrName]
)
2023-11-14 22:16:12 +08:00
2023-10-18 12:09:40 +08:00
def __clear_mesh(self):
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshWriter.')
# clear geometry
self.__mAssocMesh.clear_geometry()
# clear mtl slot because clear_geometry will not do this.
self.__mAssocMesh.materials.clear()