finish mesh reader

This commit is contained in:
yyc12345 2023-10-19 10:56:33 +08:00
parent 4f10b1a9e9
commit 16d4fc2bca

View File

@ -16,20 +16,23 @@ class FaceVertexData():
mPosIdx: int
mNmlIdx: int
mUvIdx: int
def __init__(self, pos: int = 0, nml: int = 0, uv: int = 0):
self.mPosIdx = pos
self.mNmlIdx = nml
self.mUvIdx = uv
class FaceData():
mIndices: tuple[FaceVertexData, ...]
## @remark List or tuple. List is convenient for adding and removing
mIndices: tuple[FaceVertexData] | list[FaceVertexData]
## Face used material slot index
# @remark If material slot is empty, or this face do not use material, set this value to 0.
mMtlIdx: int
def __init__(self, indices: tuple[FaceVertexData, ...] = tuple(), mtlidx: int = 0):
def __init__(self, indices: tuple[FaceVertexData] | list[FaceVertexData] = tuple(), mtlidx: int = 0):
self.mIndices = indices
self.mMtlIdx = mtlidx
def is_indices_legal(self) -> bool:
return len(self.mIndices) >= 3
@ -62,14 +65,217 @@ def _nest_custom_split_normal(nml_idx: array.array, nml_array: array.array) -> t
pos: int = idx * 3
yield (nml_array[pos], nml_array[pos + 1], nml_array[pos + 2])
class TemporaryMesh():
"""
"""
__mBindingObject: bpy.types.Object | None
__mTempMesh: bpy.types.Mesh | None
def __init__(self, binding_obj: bpy.types.Object):
self.__mBindingObject = binding_obj
self.__mTempMesh = None
if self.__mBindingObject.data is None:
raise UTIL_functions.BBPException('try getting mesh from an object without mesh.')
self.__mTempMesh = self.__mBindingObject.to_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
if self.__mTempMesh is None: return False
return True
def dispose(self):
if self.is_valid():
self.__mTempMesh = None
self.__mBindingObject.to_mesh_clear()
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
#endregion
class MeshReader():
"""
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.
A helper class TemporaryMesh can help you do this.
"""
pass
__mAssocMesh: bpy.types.Mesh | None ##< The binding mesh for this reader. None if this reader is invalid.
def __init__(self, assoc_mesh: bpy.types.Mesh):
self.__mAssocMesh = assoc_mesh
# triangulate temp mesh
if self.is_valid():
self.__triangulate_mesh()
self.__mAssocMesh.calc_normals_split()
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()
def dispose(self):
if self.is_valid():
# reset mesh
self.__mAssocMesh.free_normals_split()
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.')
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.')
cache: UTIL_virtools_types.VxVector3 = UTIL_virtools_types.VxVector3()
for nml in self.__mAssocMesh.loops:
cache.x = nml.normal.x
cache.y = nml.normal.y
cache.z = nml.normal.z
yield cache
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)
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.')
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
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)
def get_material_slot(self) -> typing.Iterator[bpy.types.Material]:
"""
@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
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)
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
# 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
# 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
# return value
yield cache
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
def __triangulate_mesh(self) -> None:
bm = bmesh.new()
bm.from_mesh(self.__mAssocMesh)
bmesh.ops.triangulate(bm, faces = bm.faces)
bm.to_mesh(self.__mAssocMesh)
bm.free()
class MeshWriter():
"""
@ -116,7 +322,7 @@ class MeshWriter():
## Same as __mFacePosIndices, but store face vertex uv index.
# Array item is int32(L). Length is equal to __mFacePosIndices
__mFaceUvIndices: array.array
## Array item is int32(L).
## Array item is int32(L).
# Length is the face count.
# It indicate how much vertex need to be consumed in __mFacePosIndices, __mFaceNmlIndices and __mFaceUvIndices for one face.
__mFaceVertexCount: array.array
@ -126,7 +332,7 @@ class MeshWriter():
# Each item is unique make sure by __mMtlSlotMap
__mMtlSlot: list[bpy.types.Material]
## The map to make sure every item in __mMtlSlot is unique.
# Key is bpy.types.Material
# Key is bpy.types.Material
# Value is key's index in __mMtlSlot.
__mMtlSlotMap: dict[bpy.types.Material, int]
@ -155,7 +361,16 @@ class MeshWriter():
def __exit__(self, exc_type, exc_value, traceback):
self.dispose()
def dispose(self):
if self.is_valid():
# write mesh
self.__write_mesh()
# reset mesh
self.__mAssocMesh = None
def add_part(self, data: MeshWriterPartData):
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshWriter.')
if not data.is_valid():
raise UTIL_functions.BBPException('invalid mesh part data.')
@ -198,14 +413,7 @@ class MeshWriter():
self.__mFaceMtlIdx.append(0)
else:
self.__mFaceMtlIdx.append(mtl_remap[mtl_idx])
def dispose(self):
if self.is_valid():
# write mesh
self.__write_mesh()
# reset mesh
self.__mAssocMesh = None
def __write_mesh(self):
# detect status
if not self.is_valid():
@ -216,7 +424,7 @@ class MeshWriter():
# push material data
for mtl in self.__mMtlSlot:
self.__mAssocMesh.materials.append(mtl)
# add corresponding count for vertex position
self.__mAssocMesh.vertices.add(len(self.__mVertexPos))
# add loops data, it is the sum count of indices
@ -228,7 +436,7 @@ class MeshWriter():
self.__mAssocMesh.uv_layers.new(do_init = False)
# split normals, it is IMPORTANT
self.__mAssocMesh.create_normals_split()
# add vertex position data
self.__mAssocMesh.vertices.foreach_set('co', self.__mVertexPos)
# add face vertex pos index data
@ -241,7 +449,7 @@ class MeshWriter():
self.__mAssocMesh.uv_layers[0].uv.foreach_set('vector',
list(_flat_face_uv_index(self.__mFaceUvIndices, self.__mVertexUV))
) # NOTE: blender 3.5 changed. UV must be visited by .uv, not the .data
# iterate face to set face data
fVertexIdx: int = 0
for fi in range(len(self.__mFaceVertexCount)):
@ -250,31 +458,31 @@ class MeshWriter():
# the loop_total will be auto calculated by the next loop_start.
# loop_total become read-only
self.__mAssocMesh.polygons[fi].loop_start = fVertexIdx
# set material index
self.__mAssocMesh.polygons[fi].material_index = self.__mFaceMtlIdx[fi]
# set auto smooth. it is IMPORTANT
# because it related to whether custom split normal can work
self.__mAssocMesh.polygons[fi].use_smooth = True
# inc vertex idx
fVertexIdx += self.__mFaceVertexCount[fi]
# validate mesh.
# it is IMPORTANT that do NOT delete custom data
# because we need use these data to set custom split normal later
self.__mAssocMesh.validate(clean_customdata = False)
# update mesh without mesh calc
self.__mAssocMesh.update(calc_edges = False, calc_edges_loose = False)
# set custom split normal data
self.__mAssocMesh.normals_split_custom_set(
tuple(_nest_custom_split_normal(self.__mFaceNmlIndices, self.__mVertexNormal))
)
# enable auto smooth. it is IMPORTANT
self.__mAssocMesh.use_auto_smooth = True
def __clear_mesh(self):
if not self.is_valid():
raise UTIL_functions.BBPException('try to call an invalid MeshWriter.')
@ -283,6 +491,6 @@ class MeshWriter():
self.__mAssocMesh.clear_geometry()
# clear mtl slot because clear_geometry will not do this.
self.__mAssocMesh.materials.clear()
class MeshUVModifier():
pass