2023-10-16 10:12:05 +08:00
|
|
|
import bpy, bmesh
|
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
|
|
|
|
|
|
|
|
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, ...]
|
|
|
|
mMtlIdx: int
|
|
|
|
|
|
|
|
def __init__(self, indices: tuple[FaceVertexData, ...] = tuple(), mtlidx: int = 0):
|
|
|
|
self.mIndices = indices
|
|
|
|
self.mMtlIdx = mtlidx
|
2023-10-17 11:50:31 +08:00
|
|
|
|
2023-10-18 12:09:40 +08:00
|
|
|
def is_indices_legal(self) -> bool:
|
|
|
|
return len(self.mIndices) >= 3
|
|
|
|
|
|
|
|
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
|
|
|
|
2023-10-18 12:09:40 +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]:
|
|
|
|
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]:
|
|
|
|
for idx in uv_idx:
|
|
|
|
pos: int = idx * 2
|
|
|
|
yield uv_array[pos]
|
|
|
|
yield uv_array[pos + 1]
|
|
|
|
|
|
|
|
def nest_custom_split_normal(nml_idx: array.array, nml_array: array.array) -> typing.Iterator[UTIL_virtools_types.ConstVxVector3]:
|
|
|
|
for idx in nml_idx:
|
|
|
|
pos: int = idx * 3
|
|
|
|
yield (nml_array[pos], nml_array[pos + 1], nml_array[pos + 2])
|
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-16 10:12:05 +08:00
|
|
|
pass
|
|
|
|
|
|
|
|
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
|
|
|
"""
|
|
|
|
|
|
|
|
class MeshWriterPartData():
|
2023-10-18 12:09:40 +08:00
|
|
|
mVertexPosition: typing.Iterator[UTIL_virtools_types.VxVector3] | None
|
|
|
|
mVertexNormal: typing.Iterator[UTIL_virtools_types.VxVector3] | None
|
|
|
|
mVertexUV: typing.Iterator[UTIL_virtools_types.VxVector2] | None
|
2023-10-17 11:50:31 +08:00
|
|
|
mFace: typing.Iterator[FaceData] | None
|
|
|
|
mMaterial: typing.Iterator[bpy.types.Material] | None
|
|
|
|
|
|
|
|
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
|
|
|
|
|
2023-10-18 12:09:40 +08:00
|
|
|
__mAssocMesh: bpy.types.Mesh | None ##< 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-17 11:50:31 +08:00
|
|
|
## Array item is int32(L).
|
|
|
|
# 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-10-17 11:50:31 +08:00
|
|
|
__mMtlSlot: list[bpy.types.Material]
|
2023-10-18 12:09:40 +08:00
|
|
|
## The map to make sure every item in __mMtlSlot is unique.
|
|
|
|
# Key is bpy.types.Material
|
|
|
|
# Value is key's index in __mMtlSlot.
|
2023-10-17 11:50:31 +08:00
|
|
|
__mMtlSlotMap: dict[bpy.types.Material, int]
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
def add_part(self, data: MeshWriterPartData):
|
|
|
|
if not data.is_valid():
|
|
|
|
raise UTIL_functions.BBPException('invalid mesh part data.')
|
|
|
|
|
|
|
|
# add vertex data
|
|
|
|
prev_vertex_pos_count: int = len(self.__mVertexPos)
|
|
|
|
self.__mVertexPos.extend(flat_vxvector3(data.mVertexPosition))
|
|
|
|
prev_vertex_nml_count: int = len(self.__mVertexNormal)
|
|
|
|
self.__mVertexNormal.extend(flat_vxvector3(data.mVertexNormal))
|
|
|
|
prev_vertex_uv_count: int = len(self.__mVertexUV)
|
|
|
|
self.__mVertexUV.extend(flat_vxvector2(data.mVertexUV))
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
if idx:
|
|
|
|
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
|
|
|
|
if mtl_idx < 0 or mtl_idx > len(mtl_remap):
|
|
|
|
# fall back. add 0
|
|
|
|
self.__mFaceMtlIdx.append(0)
|
|
|
|
else:
|
|
|
|
self.__mFaceMtlIdx.append(mtl_remap[mtl_idx])
|
2023-10-17 11:50:31 +08:00
|
|
|
|
|
|
|
def dispose(self):
|
|
|
|
if self.is_valid():
|
2023-10-18 12:09:40 +08:00
|
|
|
# write mesh
|
|
|
|
self.__write_mesh()
|
|
|
|
|
2023-10-17 11:50:31 +08:00
|
|
|
# reset mesh
|
|
|
|
self.__mAssocMesh = None
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
# add corresponding count for vertex position
|
|
|
|
self.__mAssocMesh.vertices.add(len(self.__mVertexPos))
|
|
|
|
# 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)
|
|
|
|
# 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
|
|
|
|
self.__mAssocMesh.loops.foreach_set('vertex_index', self.__mFacePosIndices)
|
|
|
|
# add face vertex nml by function
|
|
|
|
self.__mAssocMesh.loops.foreach_set('normal',
|
|
|
|
list(flat_face_nml_index(self.__mFaceNmlIndices, self.__mVertexNormal))
|
|
|
|
)
|
|
|
|
# add face vertex uv by function
|
|
|
|
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)):
|
|
|
|
# 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
|
|
|
|
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.')
|
|
|
|
|
|
|
|
# clear geometry
|
|
|
|
self.__mAssocMesh.clear_geometry()
|
|
|
|
# clear mtl slot because clear_geometry will not do this.
|
|
|
|
self.__mAssocMesh.materials.clear()
|
|
|
|
|
2023-10-16 10:12:05 +08:00
|
|
|
|
|
|
|
|
|
|
|
class MeshUVModifier():
|
|
|
|
pass
|
|
|
|
|
|
|
|
def register() -> None:
|
|
|
|
pass # nothing to register
|
|
|
|
|
|
|
|
def unregister() -> None:
|
|
|
|
pass
|