From 484a4101adf496d42dac62fbcda7fcf32dfce1a0 Mon Sep 17 00:00:00 2001 From: yyc12345 Date: Wed, 18 Oct 2023 12:09:40 +0800 Subject: [PATCH] finish blender mesh writer --- bbp_ng/UTIL_blender_mesh.py | 216 +++++++++++++++++++++++++--------- bbp_ng/UTIL_virtools_types.py | 54 +++++++++ 2 files changed, 217 insertions(+), 53 deletions(-) diff --git a/bbp_ng/UTIL_blender_mesh.py b/bbp_ng/UTIL_blender_mesh.py index 8339ade..e58796e 100644 --- a/bbp_ng/UTIL_blender_mesh.py +++ b/bbp_ng/UTIL_blender_mesh.py @@ -12,35 +12,55 @@ from . import UTIL_functions, UTIL_virtools_types #region Assist Functions -## Face used vertex index struct. -# Data member is (PosIdx, NmlIdx, UVIdx) -FaceVertexIndex = tuple[int, int, int] -## Face used vertex indices struct. -# A tuple with FaceVertexIndex member. -# At least 3 member because face need at least 3 vertex to be constructed. -FaceVertexIndices = tuple[FaceVertexIndex, ...] -## Face data struct. -# First is FaceVertexIndices struct to describe how the face constructed. -# Second is used material index. -# If you don't want to use material, you need pass None via material and point this index to it. -FaceData = tuple[FaceVertexIndices, int] +class FaceVertexData(): + mPosIdx: int + mNmlIdx: int + mUvIdx: int -def flat_vxvector3(it: typing.Iterator[UTIL_virtools_types.ConstVxVector3]) -> typing.Iterator[float]: - for entry in it: - yield entry[0] - yield entry[1] - yield entry[2] + def __init__(self, pos: int = 0, nml: int = 0, uv: int = 0): + self.mPosIdx = pos + self.mNmlIdx = nml + self.mUvIdx = uv -def flat_vxvector2(it: typing.Iterator[UTIL_virtools_types.ConstVxVector2]) -> typing.Iterator[float]: - for entry in it: - yield entry[0] - yield entry[1] +class FaceData(): + mIndices: tuple[FaceVertexData, ...] + mMtlIdx: int -def flat_vertex_indices(it: FaceVertexIndices, prev_pos: int, prev_nml: int, prev_uv: int) -> typing.Iterator[int]: + def __init__(self, indices: tuple[FaceVertexData, ...] = tuple(), mtlidx: int = 0): + self.mIndices = indices + self.mMtlIdx = mtlidx + + def is_indices_legal(self) -> bool: + return len(self.mIndices) >= 3 + +def flat_vxvector3(it: typing.Iterator[UTIL_virtools_types.VxVector3]) -> typing.Iterator[float]: for entry in it: - yield entry[0] + prev_pos - yield entry[1] + prev_nml - yield entry[2] + prev_uv + yield entry.x + yield entry.y + yield entry.z + +def flat_vxvector2(it: typing.Iterator[UTIL_virtools_types.VxVector2]) -> typing.Iterator[float]: + for entry in it: + 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]) #endregion @@ -53,13 +73,15 @@ class MeshReader(): class MeshWriter(): """ - + 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. """ class MeshWriterPartData(): - mVertexPosition: typing.Iterator[UTIL_virtools_types.ConstVxVector3] | None - mVertexNormal: typing.Iterator[UTIL_virtools_types.ConstVxVector3] | None - mVertexUV: typing.Iterator[UTIL_virtools_types.ConstVxVector2] | None + 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 mMaterial: typing.Iterator[bpy.types.Material] | None @@ -78,24 +100,34 @@ class MeshWriter(): if self.mMaterial is None: return False return True - __mAssocMesh: bpy.types.Mesh | None + __mAssocMesh: bpy.types.Mesh | None ##< The binding mesh for this writer. None if this writer is invalid. __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). - # Length must be (the sum of each items in __mFaceVertexCount) * 3. - # Item is flatten FaceVertexIndex, it mean every 3 continuous items indicate one vertex property. - __mFaceIndices: array.array + # 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 ## Array item is int32(L). # Length is the face count. - # It indicate how much vertex need to be consumed in __mFaceIndices for one face. - # The real consumed number of items in __mFaceVertexCount for each face need be multipled by 3 because __mFaceVertexCount is flat structure. + # It indicate how much vertex need to be consumed in __mFacePosIndices, __mFaceNmlIndices and __mFaceUvIndices for one face. __mFaceVertexCount: array.array __mFaceMtlIdx: array.array ##< Array item is int32(L). Length is equal to __mFaceVertexCount. + ## Material Slot. + # 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 + # Value is key's index in __mMtlSlot. __mMtlSlotMap: dict[bpy.types.Material, int] def __init__(self, assoc_mesh: bpy.types.Mesh): @@ -105,7 +137,9 @@ class MeshWriter(): self.__mVertexNormal = array.array('f') self.__mVertexUV = array.array('f') - self.__mFaceIndices = array.array('L') + self.__mFacePosIndices = array.array('L') + self.__mFaceNmlIndices = array.array('L') + self.__mFaceUvIndices = array.array('L') self.__mFaceVertexCount = array.array('L') self.__mFaceMtlIdx = array.array('L') @@ -147,34 +181,110 @@ class MeshWriter(): # add face data for face in data.mFace: # check indices count - indices = face[0] - if len(indices) < 3: + if not face.is_indices_legal(): raise UTIL_functions.BBPException('face must have at least 3 vertex.') # add indices - self.__mFaceIndices.extend( - flat_vertex_indices(indices, - prev_vertex_pos_count, - prev_vertex_nml_count, - prev_vertex_uv_count - ) - ) - self.__mFaceVertexCount.append(len(indices)) + 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)) # add face mtl with remap - mtl_idx: int = face[1] - self.__mFaceMtlIdx.append(mtl_remap[mtl_idx]) + 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]) 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(): + 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() + class MeshUVModifier(): diff --git a/bbp_ng/UTIL_virtools_types.py b/bbp_ng/UTIL_virtools_types.py index 0e369c9..f88b1a3 100644 --- a/bbp_ng/UTIL_virtools_types.py +++ b/bbp_ng/UTIL_virtools_types.py @@ -5,8 +5,62 @@ ConstVxVector2 = tuple[float, float] ConstVxVector3 = tuple[float, float, float] ConstVxVector4 = tuple[float, float, float, float] +class VxVector2(): + x: float + y: float + + def __init__(self, _x: float = 0.0, _y: float = 0.0): + self.x = _x + self.y = _y + + def from_const(self, cv: ConstVxVector2) -> None: + (self.x, self.y, ) = cv + + def to_const(self) -> ConstVxVector2: + return (self.x, self.y, ) + + # uv just need inverse y (aka. v) factor. + + def to_blender_uv(self) -> None: + self.y = -self.y + + def to_virtools_uv(self) -> None: + self.y = -self.y + + +class VxVector3(): + x: float + y: float + z: float + + def __init__(self, _x: float = 0.0, _y: float = 0.0, _z: float = 0.0): + self.x = _x + self.y = _y + self.z = _z + + def from_const(self, cv: ConstVxVector3) -> None: + (self.x, self.y, self.z) = cv + + def to_const(self) -> ConstVxVector3: + return (self.x, self.y, self.z) + + # both position and normal convertion just swap y and z factor. + + def to_blender_pos(self) -> None: + self.y, self.z = self.z, self.y + + def to_virtools_pos(self) -> None: + self.y, self.z = self.z, self.y + + def to_blender_nml(self) -> None: + self.y, self.z = self.z, self.y + + def to_virtools_nml(self) -> None: + self.y, self.z = self.z, self.y + ConstVxColorRGBA = tuple[float, float, float, float] ConstVxColorRGB = tuple[float, float, float] + class VxColor(): """ The Color struct support RGBA.