finish mesh reader
This commit is contained in:
parent
4f10b1a9e9
commit
16d4fc2bca
@ -16,20 +16,23 @@ class FaceVertexData():
|
|||||||
mPosIdx: int
|
mPosIdx: int
|
||||||
mNmlIdx: int
|
mNmlIdx: int
|
||||||
mUvIdx: int
|
mUvIdx: int
|
||||||
|
|
||||||
def __init__(self, pos: int = 0, nml: int = 0, uv: int = 0):
|
def __init__(self, pos: int = 0, nml: int = 0, uv: int = 0):
|
||||||
self.mPosIdx = pos
|
self.mPosIdx = pos
|
||||||
self.mNmlIdx = nml
|
self.mNmlIdx = nml
|
||||||
self.mUvIdx = uv
|
self.mUvIdx = uv
|
||||||
|
|
||||||
class FaceData():
|
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
|
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.mIndices = indices
|
||||||
self.mMtlIdx = mtlidx
|
self.mMtlIdx = mtlidx
|
||||||
|
|
||||||
def is_indices_legal(self) -> bool:
|
def is_indices_legal(self) -> bool:
|
||||||
return len(self.mIndices) >= 3
|
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
|
pos: int = idx * 3
|
||||||
yield (nml_array[pos], nml_array[pos + 1], nml_array[pos + 2])
|
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
|
#endregion
|
||||||
|
|
||||||
class MeshReader():
|
class MeshReader():
|
||||||
"""
|
"""
|
||||||
The passed mesh must be created by bpy.types.Object.to_mesh() and destroyed by bpy.types.Object.to_mesh_clear().
|
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.
|
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():
|
class MeshWriter():
|
||||||
"""
|
"""
|
||||||
@ -116,7 +322,7 @@ class MeshWriter():
|
|||||||
## Same as __mFacePosIndices, but store face vertex uv index.
|
## Same as __mFacePosIndices, but store face vertex uv index.
|
||||||
# Array item is int32(L). Length is equal to __mFacePosIndices
|
# Array item is int32(L). Length is equal to __mFacePosIndices
|
||||||
__mFaceUvIndices: array.array
|
__mFaceUvIndices: array.array
|
||||||
## Array item is int32(L).
|
## Array item is int32(L).
|
||||||
# Length is the face count.
|
# Length is the face count.
|
||||||
# It indicate how much vertex need to be consumed in __mFacePosIndices, __mFaceNmlIndices and __mFaceUvIndices for one face.
|
# It indicate how much vertex need to be consumed in __mFacePosIndices, __mFaceNmlIndices and __mFaceUvIndices for one face.
|
||||||
__mFaceVertexCount: array.array
|
__mFaceVertexCount: array.array
|
||||||
@ -126,7 +332,7 @@ class MeshWriter():
|
|||||||
# Each item is unique make sure by __mMtlSlotMap
|
# Each item is unique make sure by __mMtlSlotMap
|
||||||
__mMtlSlot: list[bpy.types.Material]
|
__mMtlSlot: list[bpy.types.Material]
|
||||||
## The map to make sure every item in __mMtlSlot is unique.
|
## 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.
|
# Value is key's index in __mMtlSlot.
|
||||||
__mMtlSlotMap: dict[bpy.types.Material, int]
|
__mMtlSlotMap: dict[bpy.types.Material, int]
|
||||||
|
|
||||||
@ -155,7 +361,16 @@ class MeshWriter():
|
|||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
self.dispose()
|
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):
|
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():
|
if not data.is_valid():
|
||||||
raise UTIL_functions.BBPException('invalid mesh part data.')
|
raise UTIL_functions.BBPException('invalid mesh part data.')
|
||||||
|
|
||||||
@ -198,14 +413,7 @@ class MeshWriter():
|
|||||||
self.__mFaceMtlIdx.append(0)
|
self.__mFaceMtlIdx.append(0)
|
||||||
else:
|
else:
|
||||||
self.__mFaceMtlIdx.append(mtl_remap[mtl_idx])
|
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):
|
def __write_mesh(self):
|
||||||
# detect status
|
# detect status
|
||||||
if not self.is_valid():
|
if not self.is_valid():
|
||||||
@ -216,7 +424,7 @@ class MeshWriter():
|
|||||||
# push material data
|
# push material data
|
||||||
for mtl in self.__mMtlSlot:
|
for mtl in self.__mMtlSlot:
|
||||||
self.__mAssocMesh.materials.append(mtl)
|
self.__mAssocMesh.materials.append(mtl)
|
||||||
|
|
||||||
# add corresponding count for vertex position
|
# add corresponding count for vertex position
|
||||||
self.__mAssocMesh.vertices.add(len(self.__mVertexPos))
|
self.__mAssocMesh.vertices.add(len(self.__mVertexPos))
|
||||||
# add loops data, it is the sum count of indices
|
# add loops data, it is the sum count of indices
|
||||||
@ -228,7 +436,7 @@ class MeshWriter():
|
|||||||
self.__mAssocMesh.uv_layers.new(do_init = False)
|
self.__mAssocMesh.uv_layers.new(do_init = False)
|
||||||
# split normals, it is IMPORTANT
|
# split normals, it is IMPORTANT
|
||||||
self.__mAssocMesh.create_normals_split()
|
self.__mAssocMesh.create_normals_split()
|
||||||
|
|
||||||
# add vertex position data
|
# add vertex position data
|
||||||
self.__mAssocMesh.vertices.foreach_set('co', self.__mVertexPos)
|
self.__mAssocMesh.vertices.foreach_set('co', self.__mVertexPos)
|
||||||
# add face vertex pos index data
|
# add face vertex pos index data
|
||||||
@ -241,7 +449,7 @@ class MeshWriter():
|
|||||||
self.__mAssocMesh.uv_layers[0].uv.foreach_set('vector',
|
self.__mAssocMesh.uv_layers[0].uv.foreach_set('vector',
|
||||||
list(_flat_face_uv_index(self.__mFaceUvIndices, self.__mVertexUV))
|
list(_flat_face_uv_index(self.__mFaceUvIndices, self.__mVertexUV))
|
||||||
) # NOTE: blender 3.5 changed. UV must be visited by .uv, not the .data
|
) # NOTE: blender 3.5 changed. UV must be visited by .uv, not the .data
|
||||||
|
|
||||||
# iterate face to set face data
|
# iterate face to set face data
|
||||||
fVertexIdx: int = 0
|
fVertexIdx: int = 0
|
||||||
for fi in range(len(self.__mFaceVertexCount)):
|
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.
|
# the loop_total will be auto calculated by the next loop_start.
|
||||||
# loop_total become read-only
|
# loop_total become read-only
|
||||||
self.__mAssocMesh.polygons[fi].loop_start = fVertexIdx
|
self.__mAssocMesh.polygons[fi].loop_start = fVertexIdx
|
||||||
|
|
||||||
# set material index
|
# set material index
|
||||||
self.__mAssocMesh.polygons[fi].material_index = self.__mFaceMtlIdx[fi]
|
self.__mAssocMesh.polygons[fi].material_index = self.__mFaceMtlIdx[fi]
|
||||||
|
|
||||||
# set auto smooth. it is IMPORTANT
|
# set auto smooth. it is IMPORTANT
|
||||||
# because it related to whether custom split normal can work
|
# because it related to whether custom split normal can work
|
||||||
self.__mAssocMesh.polygons[fi].use_smooth = True
|
self.__mAssocMesh.polygons[fi].use_smooth = True
|
||||||
|
|
||||||
# inc vertex idx
|
# inc vertex idx
|
||||||
fVertexIdx += self.__mFaceVertexCount[fi]
|
fVertexIdx += self.__mFaceVertexCount[fi]
|
||||||
|
|
||||||
# validate mesh.
|
# validate mesh.
|
||||||
# it is IMPORTANT that do NOT delete custom data
|
# it is IMPORTANT that do NOT delete custom data
|
||||||
# because we need use these data to set custom split normal later
|
# because we need use these data to set custom split normal later
|
||||||
self.__mAssocMesh.validate(clean_customdata = False)
|
self.__mAssocMesh.validate(clean_customdata = False)
|
||||||
# update mesh without mesh calc
|
# update mesh without mesh calc
|
||||||
self.__mAssocMesh.update(calc_edges = False, calc_edges_loose = False)
|
self.__mAssocMesh.update(calc_edges = False, calc_edges_loose = False)
|
||||||
|
|
||||||
# set custom split normal data
|
# set custom split normal data
|
||||||
self.__mAssocMesh.normals_split_custom_set(
|
self.__mAssocMesh.normals_split_custom_set(
|
||||||
tuple(_nest_custom_split_normal(self.__mFaceNmlIndices, self.__mVertexNormal))
|
tuple(_nest_custom_split_normal(self.__mFaceNmlIndices, self.__mVertexNormal))
|
||||||
)
|
)
|
||||||
# enable auto smooth. it is IMPORTANT
|
# enable auto smooth. it is IMPORTANT
|
||||||
self.__mAssocMesh.use_auto_smooth = True
|
self.__mAssocMesh.use_auto_smooth = True
|
||||||
|
|
||||||
def __clear_mesh(self):
|
def __clear_mesh(self):
|
||||||
if not self.is_valid():
|
if not self.is_valid():
|
||||||
raise UTIL_functions.BBPException('try to call an invalid MeshWriter.')
|
raise UTIL_functions.BBPException('try to call an invalid MeshWriter.')
|
||||||
@ -283,6 +491,6 @@ class MeshWriter():
|
|||||||
self.__mAssocMesh.clear_geometry()
|
self.__mAssocMesh.clear_geometry()
|
||||||
# clear mtl slot because clear_geometry will not do this.
|
# clear mtl slot because clear_geometry will not do this.
|
||||||
self.__mAssocMesh.materials.clear()
|
self.__mAssocMesh.materials.clear()
|
||||||
|
|
||||||
class MeshUVModifier():
|
class MeshUVModifier():
|
||||||
pass
|
pass
|
||||||
|
Loading…
Reference in New Issue
Block a user