├── LICENSE ├── .gitignore ├── README.md ├── XGenUEGroomExporter_py2.py ├── XGenUEGroomExporter.py ├── XGenDescriptionUEGroomExporter_py2.py └── XGenDescriptionUEGroomExporter.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 PDE26jjk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XGenUEGroomExporter 2 | A small tool to export XGen to UE Groom. Select curves and interactive XGen descriptions to export an abc file. The new script can directly export XGen descriptions with guides. 3 | 4 | 选择曲线和可交互XGen描述,可以导出abc文件。带分组,可以烘焙UV。UE5可识别。新脚本可以直接导出带有导向的XGen描述。 5 | 6 | ![image-20241108002757544](https://raw.githubusercontent.com/PDE26jjk/misc/main/img/image-20241108002757544.png) 7 | 8 | Maya2025版本可用。[用法视频](https://www.bilibili.com/video/BV1U7mzYDEA4) 9 | 10 | 现在只维护XGenUEGroomExporter.py和XGenDescriptionUEGroomExporter.py及其py2版本。 11 | 12 | 微信: XJ845077205 13 | 邮箱: 26jjk@sina.com 14 | 15 | ## 更新功能: 16 | 17 | - 无需numpy也可以使用 18 | - XGen写入宽度 19 | - 自动生成Group_id,以便低版本UE可以识别分组 20 | - 写入动画 21 | - 增加py2版本,Maya2018可用。 22 | - knots端点处修复,解决UE导入报错 23 | - 新增XGenDescriptionUEGroomExporter.py,专门导出XGen Description。 24 | - XGenDescriptionUEGroomExporter列表中增添多选功能 2025-3-15 25 | 26 | ## XGenUEGroomExporter用法说明: 27 | 28 | ![image-20250114225246322](https://raw.githubusercontent.com/PDE26jjk/misc/main/img/image-20250114225246322.png) 29 | 30 | 这个脚本将曲线和可交互XGen导出成UE可识别的Groom。 31 | 32 | 选择曲线和可交互XGen之后,点击Refresh selected按钮,它们会显示在界面中。将需要作为导向的曲线和对应的可交互XGen设置一样的Group name,曲线勾选Is guide即可。 33 | 34 | UV烘焙:将会识别XGen的绑定Mesh,可以按Pick other mesh选择其他Mesh。 35 | 36 | 选择Animation之后,可以导出动画,需要在Animation选项卡中设置帧范围。如果勾选Preroll,将在记录动画之前从第一帧播放到帧范围开始。 37 | 38 | ## XGenDescriptionUEGroomExporter用法说明: 39 | 40 | ![image-20250114223649517](https://raw.githubusercontent.com/PDE26jjk/misc/main/img/image-20250114223649517.png) 41 | 42 | 这个脚本专门将带有导向的XGen描述导出成UE可识别的Groom。 43 | 44 | 选择带有导向的XGen描述之后,点击Refresh selected按钮,它们会显示在界面中。点击每个描述,右侧显示其他选项。 45 | 46 | 如果勾选Animation,将会导出导向动画。 47 | 48 | write spline animation 选项:如果勾选,导出动画的时候同时导出Spline的动画,文件可能会很大。 49 | 50 | write guide id from ptex 选项:选择一张ptex贴图,将会将对应颜色的导向和发丝对应起来,生成UE可识别的属性。默认从Clumping修改器读取。!!注意:UE5.3以上的版本目前有bug,不会正确读取导向权重属性。 51 | 52 | 生成过程中产生的交互式XGen会被放到一个父节点中,可以按Clear Temp Data删除它。 53 | 54 | # English version 55 | ## Usage Instructions for XGenUEGroomExporter: 56 | 57 | ![image-20250114225246322](https://raw.githubusercontent.com/PDE26jjk/misc/main/img/image-20250114225246322.png) 58 | 59 | This script exports curves and interactive XGen descriptions into UE-recognizable Groom format. 60 | 61 | After selecting the curves and interactive XGen descriptions, click the **Refresh Selected** button; they will be displayed in the interface. Assign the same **Group Name** to the curves intended as guides and their corresponding interactive XGen descriptions, and check the **Is Guide** option for the curves. 62 | 63 | **UV Baking**: It will recognize the bound mesh of the XGen description; you can select another mesh by clicking **Pick Other Mesh**. 64 | 65 | Once **Animation** is selected, animated data can be exported, with the frame range to be set in the **Animation** tab. If **Preroll** is checked, it will play from the first frame to the start of the frame range before recording the animation. 66 | 67 | ## Usage Instructions for XGenDescriptionUEGroomExporter: 68 | 69 | ![image-20250114223649517](https://raw.githubusercontent.com/PDE26jjk/misc/main/img/image-20250114223649517.png) 70 | 71 | This script specifically exports XGen descriptions with guides into UE-recognizable Groom format. 72 | 73 | After selecting the XGen descriptions with guides, click the **Refresh Selected** button; they will be displayed in the interface. Click on each description to reveal additional options on the right. 74 | 75 | If **Animation** is checked, guide animation will be exported. 76 | 77 | **Write Spline Animation** option: If checked, the spline animation will be exported simultaneously with the animation, potentially resulting in a large file size. 78 | 79 | **Write Guide ID from Ptex** option: Select a ptex texture; it will correlate the guide corresponding to the color with the hair strand, generating UE-recognizable attributes. By default, it reads from the Clumping modifier. **Note**: There is currently a bug in UE 5.3 and above that prevents the correct reading of guide weight attributes. 80 | 81 | The interactive XGen generated during the process will be placed under a parent node, which can be deleted by clicking **Clear Temp Data**. -------------------------------------------------------------------------------- /XGenUEGroomExporter_py2.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import alembic.Abc as abc 3 | import alembic.AbcGeom as abcGeom 4 | import alembic.AbcCoreAbstract as abcA 5 | import maya.OpenMaya as om1 6 | import maya.api.OpenMayaAnim as omAnim 7 | import maya.api.OpenMaya as om 8 | import imath 9 | import array 10 | import struct 11 | import zlib 12 | import json 13 | import maya.cmds as cmds 14 | import time 15 | 16 | # %% 17 | print_debug = False 18 | 19 | 20 | # %% 21 | def list2ImathArray(l, _type): 22 | arr = _type(len(l)) 23 | for i in range(len(l)): 24 | arr[i] = l[i] 25 | return arr 26 | 27 | 28 | def floatList2V3fArray(l): 29 | arr = imath.V3fArray(len(l) // 3) 30 | for i in range(len(arr)): 31 | arr[i].x = l[i * 3] 32 | arr[i].y = l[i * 3 + 1] 33 | arr[i].z = l[i * 3 + 2] 34 | return arr 35 | 36 | 37 | # %% 38 | def getXgenData(fnDepNode): 39 | splineData = fnDepNode.findPlug("outSplineData", False) 40 | 41 | handle = splineData.asMObject() 42 | mdata = om.MFnPluginData(handle) 43 | mData = mdata.data() 44 | 45 | rawData = mData.writeBinary() 46 | 47 | def GetBlocks(bype_data): 48 | address = 0 49 | i = 0 50 | blocks = [] 51 | maxIt = 100 52 | while address < len(bype_data) - 1: 53 | # size = int.from_bytes(bype_data[address + 8:address + 16], byteorder='little', signed=False) 54 | # type_code = int.from_bytes(bype_data[address:address + 4], byteorder='little', signed=False) 55 | size = struct.unpack(' maxIt: 61 | break 62 | return blocks 63 | 64 | dataBlocks = GetBlocks(rawData) 65 | headerBlock = dataBlocks[0] 66 | dataBlocks.pop(0) 67 | 68 | dataString = str(rawData[headerBlock[0]:headerBlock[1]]) 69 | dataJson = json.loads(dataString) 70 | # print(dataJson) 71 | Header = dataJson['Header'] 72 | 73 | Items = dict() 74 | 75 | def readItems(items): 76 | for k, v in items: 77 | if isinstance(v, (int, long)): 78 | group = v >> 32 79 | index = v & 0xFFFFFFFF 80 | addr = (group, index) 81 | if k not in Items: 82 | Items[k] = [addr] 83 | else: 84 | Items[k].append(addr) 85 | 86 | for i in range(len(dataJson['Items'])): 87 | readItems(dataJson['Items'][i].items()) 88 | for i in range(len(dataJson['RefMeshArray'])): 89 | readItems(dataJson['RefMeshArray'][i].items()) 90 | 91 | # print(Items) 92 | decompressedData = dict() 93 | 94 | def decompressData(group, index): 95 | if group not in decompressedData: 96 | if Header['GroupBase64']: 97 | raise Exception("我还没有碰到Base64的情况,请提醒我更新代码") 98 | if Header['GroupDeflate']: 99 | validData = zlib.decompress(str(rawData[dataBlocks[group][0] + 32:])) 100 | else: 101 | validData = rawData[dataBlocks[group][0]:dataBlocks[group][1]] 102 | decompressedData[group] = validData 103 | else: 104 | validData = decompressedData[group] 105 | blocks = GetBlocks(validData) 106 | return validData[blocks[index][0]:blocks[index][1]] 107 | 108 | PrimitiveInfosList = [] 109 | PositionsDataList = [] 110 | WidthsDataList = [] 111 | for k, v in Items.items(): 112 | # print(k,len(v)) 113 | if k == 'PrimitiveInfos': 114 | dtype_format = ' 1: 219 | knotsLength = len(knotsArray) 220 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 221 | knotsArray[0] == knotsArray[1]): 222 | knots.append(float(knotsArray[0])) 223 | else: 224 | knots.append(float(2 * knotsArray[0] - knotsArray[1])) 225 | 226 | for j in range(knotsLength): 227 | knots.append(float(knotsArray[j])) 228 | 229 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 230 | knotsArray[knotsLength - 1] == knotsArray[knotsLength - 2]): 231 | knots.append(float(knotsArray[knotsLength - 1])) 232 | else: 233 | knots.append(float(2 * knotsArray[knotsLength - 1] - knotsArray[knotsLength - 2])) 234 | samp.setCurvesNumVertices(nVertices) 235 | samp.setPositions(floatList2V3fArray(pointslist)) 236 | samp.setOrders(list2ImathArray(orders, imath.UnsignedCharArray)) 237 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 238 | 239 | # widths = list2ImathArray([0.1], imath.FloatArray) 240 | # widths = abc.Float32TPTraits() 241 | # widths = abcGeom.OFloatGeomParamSample(widths, abcGeom.GeometryScope.kConstantScope) 242 | # samp.setWidths(widths) 243 | self.schema.set(samp) 244 | 245 | def write_frame(self): 246 | numCurves = len(self.curves) 247 | if numCurves == 0: 248 | return 249 | curve = om.MFnNurbsCurve(self.curves[0]) 250 | 251 | samp = abcGeom.OCurvesSchemaSample() 252 | samp.setBasis(self.firstSamp.getBasis()) 253 | samp.setWrap(self.firstSamp.getWrap()) 254 | samp.setType(self.firstSamp.getType()) 255 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 256 | samp.setOrders(self.firstSamp.getOrders()) 257 | samp.setKnots(self.firstSamp.getKnots()) 258 | 259 | pointslist = [] 260 | for i in range(numCurves): 261 | curve = curve.setObject(self.curves[i]) 262 | numCVs = curve.numCVs 263 | cvArray = curve.cvPositions() 264 | for j in range(numCVs): 265 | pointslist.append(cvArray[j].x) 266 | pointslist.append(cvArray[j].y) 267 | pointslist.append(cvArray[j].z) 268 | 269 | samp.setPositions(floatList2V3fArray(pointslist)) 270 | 271 | self.schema.set(samp) 272 | 273 | def bake_uv(self, bakeMesh, uv_set=None): 274 | if self.hairRootList is None: 275 | return 276 | if bakeMesh is None: 277 | return 278 | if uv_set is None: 279 | uv_set = bakeMesh.currentUVSetName() 280 | elif uv_set not in bakeMesh.getUVSetNames(): 281 | raise Exception('Invalid UV Set : {}'.format(uv_set)) 282 | 283 | uvs = imath.V2fArray(len(self.hairRootList)) 284 | for i, hairRoot in enumerate(self.hairRootList): 285 | res = bakeMesh.getUVAtPoint(hairRoot, om.MSpace.kWorld, uvSet=uv_set) 286 | uvs[i].x = res[0] 287 | uvs[i].y = res[1] 288 | 289 | cp = self.schema.getArbGeomParams() 290 | uv_prop = abc.OV2fArrayProperty(cp, "groom_root_uv") 291 | uv_prop.setValue(uvs) 292 | 293 | 294 | class XGenProxy(CurvesProxy): 295 | def __init__(self, curveObj, fnDepNode, needBakeUV=False, animation=False): 296 | super(XGenProxy, self).__init__(curveObj, fnDepNode, needBakeUV, animation) 297 | 298 | def write_first_frame(self): 299 | if print_debug: 300 | startTime = time.time() 301 | PrimitiveInfosList, PositionsDataList, WidthsDataList = getXgenData(self.fnDepNode) 302 | if print_debug: 303 | print("getXgenData: %.4f" % (time.time() - startTime)) 304 | startTime = time.time() 305 | numCurves = 0 306 | numCVs = 0 307 | for i, PrimitiveInfos in enumerate(PrimitiveInfosList): 308 | numCurves += len(PrimitiveInfos) 309 | for PrimitiveInfo in PrimitiveInfos: 310 | numCVs += PrimitiveInfo[1] 311 | 312 | orders = imath.UnsignedCharArray(numCurves) 313 | nVertices = imath.IntArray(numCurves) 314 | cp = self.schema.getArbGeomParams() 315 | 316 | samp = self.firstSamp 317 | samp.setBasis(abcGeom.BasisType.kBsplineBasis) 318 | samp.setWrap(abcGeom.CurvePeriodicity.kNonPeriodic) 319 | samp.setType(abcGeom.CurveType.kCubic) 320 | 321 | degree = 3 322 | pointArray = imath.V3fArray(numCVs) 323 | widthArray = imath.FloatArray(numCVs) 324 | if self.needBakeUV: 325 | self.hairRootList = [] 326 | knots = [] 327 | 328 | curveIndex = 0 329 | cvIndex = 0 330 | 331 | for j in range(len(PrimitiveInfosList)): 332 | PrimitiveInfos = PrimitiveInfosList[j] 333 | posData = PositionsDataList[j] 334 | widthData = WidthsDataList[j] 335 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 336 | offset = PrimitiveInfo[0] 337 | length = int(PrimitiveInfo[1]) 338 | if length < 2: 339 | continue 340 | startAddr = offset * 3 341 | for k in range(length): 342 | pointArray[cvIndex].x = posData[startAddr] 343 | pointArray[cvIndex].y = posData[startAddr + 1] 344 | pointArray[cvIndex].z = posData[startAddr + 2] 345 | if k == 0 and self.needBakeUV: 346 | self.hairRootList.append(om.MPoint(pointArray[cvIndex])) 347 | widthArray[cvIndex] = widthData[offset + k] 348 | startAddr += 3 349 | cvIndex += 1 350 | 351 | orders[curveIndex] = degree + 1 352 | nVertices[curveIndex] = length 353 | 354 | knotsInsideNum = length - degree + 1 355 | # knotsList = [*([0] * (degree - 1)), *list(range(knotsInsideNum)), *([knotsInsideNum - 1] * (degree - 1))] 356 | knotsList = [0] * degree + list(range(knotsInsideNum)) + [knotsInsideNum - 1] * degree 357 | knots += knotsList 358 | curveIndex += 1 359 | 360 | samp.setCurvesNumVertices(nVertices) 361 | samp.setPositions(pointArray) 362 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 363 | samp.setOrders(orders) 364 | 365 | # bake vertex color example 366 | # cvColor = abcGeom.OC3fGeomParam(cp, "groom_color", False, abcGeom.GeometryScope.kVertexScope, 1) 367 | # cvColorArray = imath.C3fArray(len(pointslist) // 3) 368 | # i = 0 369 | # color1 = imath.Color3f((0, 1, 1)); 370 | # color2 = imath.Color3f((1, 0, 1)) 371 | # for _ in range(len(nVertices)): 372 | # length = nVertices[_] 373 | # for j in range(length): 374 | # t = (j / (length - 1)) 375 | # cvColorArray[i] = color1 * (1 - t) + color2 * t 376 | # i += 1 377 | # cvColorArray = abcGeom.OC3fGeomParamSample(cvColorArray, abcGeom.GeometryScope.kVertexScope) 378 | # cvColor.set(cvColorArray) 379 | 380 | # write width 381 | widths = abcGeom.OFloatGeomParamSample(widthArray, abcGeom.GeometryScope.kVertexScope) 382 | samp.setWidths(widths) 383 | self.schema.set(samp) 384 | 385 | if print_debug: 386 | print("write_first_frame: %.4f" % (time.time() - startTime)) 387 | 388 | def write_frame(self): 389 | if print_debug: 390 | startTime = time.time() 391 | PrimitiveInfosList, PositionsDataList, WidthsDataList = getXgenData(self.fnDepNode) 392 | numCurves = 0 393 | numCVs = 0 394 | for i, PrimitiveInfos in enumerate(PrimitiveInfosList): 395 | numCurves += len(PrimitiveInfos) 396 | for PrimitiveInfo in PrimitiveInfos: 397 | numCVs += PrimitiveInfo[1] 398 | 399 | cp = self.schema.getArbGeomParams() 400 | 401 | samp = abcGeom.OCurvesSchemaSample() 402 | samp.setBasis(self.firstSamp.getBasis()) 403 | samp.setWrap(self.firstSamp.getWrap()) 404 | samp.setType(self.firstSamp.getType()) 405 | 406 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 407 | samp.setKnots(self.firstSamp.getKnots()) 408 | samp.setOrders(self.firstSamp.getOrders()) 409 | samp.setWidths(self.firstSamp.getWidths()) 410 | 411 | pointArray = imath.V3fArray(numCVs) 412 | 413 | curveIndex = 0 414 | cvIndex = 0 415 | for j in range(len(PrimitiveInfosList)): 416 | PrimitiveInfos = PrimitiveInfosList[j] 417 | posData = PositionsDataList[j] 418 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 419 | offset = PrimitiveInfo[0] 420 | length = int(PrimitiveInfo[1]) 421 | if length < 2: 422 | continue 423 | startAddr = offset * 3 424 | for k in range(length): 425 | pointArray[cvIndex].x = posData[startAddr] 426 | pointArray[cvIndex].y = posData[startAddr + 1] 427 | pointArray[cvIndex].z = posData[startAddr + 2] 428 | startAddr += 3 429 | cvIndex += 1 430 | 431 | curveIndex += 1 432 | 433 | samp.setPositions(pointArray) 434 | 435 | self.schema.set(samp) 436 | if print_debug: 437 | print("write_frame: %.4f" % (time.time() - startTime)) 438 | 439 | 440 | # %% 441 | try: 442 | from PySide6 import QtCore, QtWidgets, QtGui 443 | import shiboken6 as shiboken 444 | except: 445 | from PySide2 import QtCore, QtWidgets, QtGui 446 | import shiboken2 as shiboken 447 | 448 | import maya.OpenMayaUI as om1ui 449 | 450 | 451 | def mayaWindow(): 452 | main_window_ptr = om1ui.MQtUtil.mainWindow() 453 | return shiboken.wrapInstance(int(main_window_ptr), QtWidgets.QWidget) 454 | 455 | 456 | # %% 457 | class SaveXGenWindow(QtWidgets.QDialog): 458 | class Content: 459 | def __init__(self, fnDepNode, showName, Type, groupName, isGuide, bakeUV, animation, export): 460 | self.showName = showName 461 | self.fnDepNode = fnDepNode 462 | self.Type = Type 463 | self.groupName = QtWidgets.QLineEdit() 464 | self.groupName.setText(groupName) 465 | self.isGuide = QtWidgets.QCheckBox() 466 | self.isGuide.setChecked(isGuide) 467 | self.bakeUV = QtWidgets.QCheckBox() 468 | self.bakeUV.setChecked(bakeUV) 469 | self.animation = QtWidgets.QCheckBox() 470 | self.animation.setChecked(animation) 471 | self.export = QtWidgets.QCheckBox() 472 | self.export.setChecked(export) 473 | 474 | curveType = "curve" 475 | xgenType = "xgen" 476 | 477 | def __init__(self, parent=mayaWindow()): 478 | super(SaveXGenWindow, self).__init__(parent) 479 | self.contentList = [] 480 | self.save_path = '.' 481 | self.bakeMesh = None 482 | self.setWindowTitle("Export XGen to UE Groom") 483 | self.setGeometry(400, 400, 850, 450) 484 | self.buildUI() 485 | 486 | def showAbout(self): 487 | QtWidgets.QMessageBox.about(self, "Export XGen to UE Groom", 488 | "A small tool to export XGen to UE Groom, by PDE26jjk.
link: https://github.com/PDE26jjk/XGenUEGroomExporter") 489 | 490 | def createFrame(self, labelText): 491 | try: 492 | frame = om1ui.MQtUtil.findControl( 493 | cmds.frameLayout(label=labelText, collapsable=True, collapse=True, manage=True)) 494 | frame = shiboken.wrapInstance(int(frame), QtWidgets.QWidget) 495 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 496 | frameLayout = frame.children()[2].children()[0] 497 | except: 498 | frame = QtWidgets.QFrame(self) 499 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 500 | frameLayout = QtWidgets.QVBoxLayout(frame) 501 | frame.children().append(frameLayout) 502 | return frame, frameLayout 503 | 504 | def buildUI(self): 505 | main_layout = QtWidgets.QVBoxLayout() 506 | 507 | menu_bar = QtWidgets.QMenuBar(self) 508 | menu_bar.addMenu("Help").addAction("About", self.showAbout) 509 | main_layout.setMenuBar(menu_bar) 510 | 511 | label1 = QtWidgets.QLabel("Please select Curves and Interactive XGen") # 请选择曲线和交互式XGen 512 | label1.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 513 | hBox = QtWidgets.QHBoxLayout() 514 | hBox.setContentsMargins(10, 4, 10, 4) 515 | hBox.addWidget(label1) 516 | 517 | self.fillWithSelectList_button = QtWidgets.QPushButton("Refresh selected") # 刷新选择 518 | self.fillWithSelectList_button.clicked.connect(self.fillWithSelectList) 519 | self.fillWithSelectList_button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 520 | # main_layout.addWidget(self.fillWithSelectList_button) 521 | hBox.addStretch(1) 522 | hBox.addWidget(self.fillWithSelectList_button) 523 | main_layout.addLayout(hBox) 524 | 525 | self.table = QtWidgets.QTableWidget(self) 526 | self.table.setColumnCount(8) # 设置列数 527 | self.table.setHorizontalHeaderLabels(["", "Name", "Type", "Group name", "Is guide", "Bake UV", "Animation", ""]) 528 | self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) 529 | self.table.setColumnWidth(0, 40) 530 | self.table.horizontalHeader().setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) 531 | self.table.setColumnWidth(4, 140) 532 | self.table.horizontalHeader().setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) 533 | self.table.setColumnWidth(5, 140) 534 | self.table.horizontalHeader().setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) 535 | self.table.setColumnWidth(6, 140) 536 | self.table.horizontalHeader().setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) 537 | self.table.horizontalHeader().setStretchLastSection(True) 538 | 539 | self.table.setStyleSheet(""" 540 | QTableView::item 541 | { 542 | border: 0px; 543 | padding: 5px; 544 | background-color: rgb(68, 68, 68); 545 | } 546 | QTableView::item QCheckBox { 547 | padding-left:60px; 548 | } 549 | """) 550 | 551 | self.table.clearContents() 552 | self.table.setRowCount(0) 553 | 554 | self.Bakeframe, frameLayout = self.createFrame(labelText="Bake UV") 555 | 556 | self.MeshName = QtWidgets.QLabel("Mesh : ---") 557 | hBox = QtWidgets.QHBoxLayout() 558 | hBox.setContentsMargins(10, 10, 10, 10) 559 | hBox.addWidget(self.MeshName) 560 | hBox2 = QtWidgets.QHBoxLayout() 561 | label = QtWidgets.QLabel("UV Set : ") 562 | hBox2.addWidget(label) 563 | self.combo = QtWidgets.QComboBox() 564 | self.combo.addItem(" --- ") 565 | 566 | self.uvSetStr = QtWidgets.QLabel("Selected: None") 567 | 568 | self.combo.currentIndexChanged.connect(self.update_label) 569 | hBox2.addWidget(self.combo) 570 | hBox.addStretch(2) 571 | hBox.addLayout(hBox2) 572 | hBox.addStretch(1) 573 | 574 | frameLayout.addLayout(hBox) 575 | 576 | self.button3 = QtWidgets.QPushButton("Pick other mesh", self) 577 | self.button3.clicked.connect(self.pick_mesh) 578 | self.button3.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 579 | frameLayout.addWidget(self.button3) 580 | 581 | self.separator = QtWidgets.QFrame(self) 582 | self.separator.setFrameShape(QtWidgets.QFrame.HLine) 583 | self.separator.setFrameShadow(QtWidgets.QFrame.Sunken) 584 | 585 | self.AnimationFrame, frameLayout = self.createFrame(labelText="Animation") 586 | 587 | validator = QtGui.QIntValidator() 588 | validator.setRange(0, 99999) 589 | self.startFrame = QtWidgets.QLineEdit() 590 | self.startFrame.setMaximumWidth(60) 591 | self.startFrame.setValidator(validator) 592 | self.startFrame.setText(str(0)) 593 | self.endFrame = QtWidgets.QLineEdit() 594 | self.endFrame.setMaximumWidth(60) 595 | self.endFrame.setValidator(validator) 596 | self.endFrame.setText(str(0)) 597 | self.preroll = QtWidgets.QCheckBox("Preroll") 598 | 599 | frameLayout.setContentsMargins(10, 10, 10, 10) 600 | hBox = QtWidgets.QHBoxLayout() 601 | hBox.addWidget(QtWidgets.QLabel("Frame Range : ")) 602 | hBox.addWidget(self.startFrame) 603 | hBox.addWidget(QtWidgets.QLabel(" ~ ")) 604 | hBox.addWidget(self.endFrame) 605 | hBox.addStretch(1) 606 | hBox2 = QtWidgets.QHBoxLayout() 607 | hBox2.addWidget(self.preroll) 608 | hBox2.addStretch(1) 609 | 610 | frameLayout.addLayout(hBox) 611 | frameLayout.addLayout(hBox2) 612 | 613 | self.SettingFrame, frameLayout = self.createFrame(labelText="Setting") 614 | 615 | frameLayout.setContentsMargins(10, 10, 10, 10) 616 | hBox = QtWidgets.QHBoxLayout() 617 | self.createGroupId_cb = QtWidgets.QCheckBox("Create group id") 618 | self.createGroupId_cb.setChecked(True) 619 | hBox.addWidget(self.createGroupId_cb) 620 | 621 | frameLayout.addLayout(hBox) 622 | 623 | self.save_button = QtWidgets.QPushButton("Save Alembic File", self) 624 | self.save_button.clicked.connect(self.save_abc) 625 | 626 | self.cancel_button = QtWidgets.QPushButton("Close", self) # 关闭 627 | self.cancel_button.clicked.connect(self.close) # 关闭窗口 628 | 629 | button_layout = QtWidgets.QHBoxLayout() 630 | button_layout.addWidget(self.save_button) 631 | button_layout.addWidget(self.cancel_button) 632 | 633 | main_layout.addWidget(self.table) 634 | main_layout.addWidget(self.Bakeframe) 635 | main_layout.addWidget(self.AnimationFrame) 636 | main_layout.addWidget(self.SettingFrame) 637 | main_layout.addWidget(self.separator) 638 | main_layout.addLayout(button_layout) 639 | 640 | self.setLayout(main_layout) 641 | 642 | def pick_mesh(self): 643 | selectionList = om.MGlobal.getActiveSelectionList() 644 | if selectionList.length() > 0: 645 | dag_path = selectionList.getDagPath(0) 646 | fnDepNode = om.MFnDependencyNode(dag_path.node()) 647 | itDag = om.MItDag() 648 | # find mesh 649 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kMesh) 650 | while not itDag.isDone(): 651 | meshPath = om.MDagPath.getAPathTo(itDag.currentItem()) 652 | mesh = om.MFnMesh(meshPath) 653 | self.setBakeMesh(mesh) 654 | break 655 | 656 | def update_label(self): 657 | selected_option = self.combo.currentText() 658 | self.uvSetStr.setText(selected_option) 659 | 660 | def save_abc(self): 661 | if len(self.contentList) == 0: 662 | print("No content") 663 | return 664 | file_path = cmds.fileDialog2( 665 | # dialogStyle=2, 666 | caption="Save as Alembic File", 667 | fileMode=0, 668 | okCaption="save", 669 | # defaultExtension='abc', 670 | startingDirectory=self.save_path, 671 | ff='Alembic Files (*.abc);;All Files (*)' 672 | ) 673 | if file_path: 674 | self.save_path = file_path[0] 675 | else: 676 | return 677 | startTime = time.time() 678 | oldCurTime = omAnim.MAnimControl.currentTime() 679 | archive = abc.OArchive(str(file_path[0])) 680 | 681 | anyAnimation = False 682 | for item in self.contentList: 683 | hasAnimation = item.animation.isChecked() 684 | if hasAnimation: 685 | anyAnimation = True 686 | break 687 | if anyAnimation: 688 | frameRange = [int(self.startFrame.text()), int(self.endFrame.text())] 689 | if (frameRange[0] > frameRange[1] 690 | or frameRange[0] < omAnim.MAnimControl.minTime().value 691 | or frameRange[1] > omAnim.MAnimControl.maxTime().value): 692 | raise ValueError("Frame out of range.") 693 | # frameRange[0] = int(max(frameRange[0], omAnim.MAnimControl.minTime().value)) 694 | # frameRange[1] = int(min(frameRange[1], omAnim.MAnimControl.maxTime().value)) 695 | 696 | sec = om.MTime(1, om.MTime.kSeconds) 697 | spf = 1.0 / sec.asUnits(om.MTime.uiUnit()) 698 | timeSampling = abcA.TimeSampling(spf, spf * frameRange[0]) 699 | 700 | timeIndex = archive.addTimeSampling(timeSampling) 701 | 702 | proxyList = [] 703 | for item in self.contentList: 704 | if not item.export: 705 | continue 706 | fnDepNode = item.fnDepNode 707 | needBakeUV = item.bakeUV.isChecked() 708 | hasAnimation = item.animation.isChecked() 709 | if hasAnimation: 710 | curveObj = abcGeom.OCurves(archive.getTop(), str(fnDepNode.name()), timeIndex) 711 | else: 712 | curveObj = abcGeom.OCurves(archive.getTop(), str(fnDepNode.name())) 713 | 714 | if item.Type == SaveXGenWindow.xgenType: 715 | proxy = XGenProxy(curveObj, fnDepNode, needBakeUV, hasAnimation) 716 | elif item.Type == SaveXGenWindow.curveType: 717 | proxy = CurvesProxy(curveObj, fnDepNode, needBakeUV, hasAnimation) 718 | else: 719 | continue 720 | proxyList.append(proxy) 721 | proxy.write_group_name(item.groupName.text()) 722 | proxy.write_is_guide(item.isGuide.isChecked()) 723 | 724 | if len(proxyList) == 0: 725 | print("No content") 726 | return 727 | 728 | if self.createGroupId_cb.isChecked(): 729 | groupIds = dict() 730 | currentId = 0 731 | for proxy in proxyList: 732 | if proxy.groupName not in groupIds: 733 | groupIds[proxy.groupName] = currentId 734 | currentId += 1 735 | proxy.write_group_id(groupIds[proxy.groupName]) 736 | 737 | if anyAnimation: 738 | if self.preroll.isChecked(): 739 | for frame in range(int(omAnim.MAnimControl.minTime().value), frameRange[0]): 740 | om1.MGlobal.viewFrame(frame) 741 | for frame in range(frameRange[0], frameRange[1] + 1): 742 | om1.MGlobal.viewFrame(frame) 743 | for item in proxyList: 744 | if frame == frameRange[0]: 745 | item.write_first_frame() 746 | elif item.animation: 747 | item.write_frame() 748 | omAnim.MAnimControl.setCurrentTime(oldCurTime) 749 | else: 750 | for item in proxyList: 751 | item.write_first_frame() 752 | for item in proxyList: 753 | item.bake_uv(self.bakeMesh, self.uvSetStr.text()) 754 | print("Data has been saved in %s, it took %.2f seconds." % (file_path[0], time.time() - startTime)) 755 | 756 | return file_path[0] 757 | 758 | def fillWithSelectList(self): 759 | self.contentList = [] 760 | selectionList = om.MGlobal.getActiveSelectionList() 761 | contentList = [] 762 | for i in range(selectionList.length()): 763 | dag_path = selectionList.getDagPath(i) 764 | fnDepNode = om.MFnDependencyNode(dag_path.node()) 765 | itDag = om.MItDag() 766 | # find xgen description 767 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kNamedObject) 768 | xgDes = None 769 | while not itDag.isDone(): 770 | dn = om.MFnDependencyNode(itDag.currentItem()) 771 | if dn.typeName == 'xgmSplineDescription': 772 | xgDes = dn 773 | break 774 | itDag.next() 775 | if xgDes is not None: 776 | contentList.append( 777 | SaveXGenWindow.Content(xgDes, fnDepNode.name(), SaveXGenWindow.xgenType, fnDepNode.name(), False, 778 | False, False, True)) 779 | boundMesh = self.findBoundMesh(xgDes) 780 | if boundMesh is not None: 781 | self.setBakeMesh(boundMesh) 782 | continue 783 | # find curve 784 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kCurve) 785 | isCurve = False 786 | while not itDag.isDone(): 787 | isCurve = True 788 | break 789 | if isCurve: 790 | groupNameStr = fnDepNode.name() 791 | suffix = "_guide" 792 | if groupNameStr.endswith(suffix): 793 | groupNameStr = groupNameStr[:-len(suffix)] 794 | contentList.append( 795 | SaveXGenWindow.Content(fnDepNode, fnDepNode.name(), SaveXGenWindow.curveType, groupNameStr, True, 796 | False, False, True)) 797 | 798 | self.table.setRowCount(len(contentList)) 799 | for row in range(len(contentList)): 800 | self.table.setCellWidget(row, 0, contentList[row].export) 801 | contentList[row].export.setStyleSheet("padding-left:8px") 802 | item = QtWidgets.QTableWidgetItem(contentList[row].showName) 803 | # item.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 804 | item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) 805 | self.table.setItem(row, 1, item) 806 | 807 | item = QtWidgets.QTableWidgetItem(contentList[row].Type) 808 | # item.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 809 | item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) 810 | self.table.setItem(row, 2, item) 811 | 812 | self.table.setCellWidget(row, 3, contentList[row].groupName) 813 | self.table.setCellWidget(row, 4, contentList[row].isGuide) 814 | self.table.setCellWidget(row, 5, contentList[row].bakeUV) 815 | self.table.setCellWidget(row, 6, contentList[row].animation) 816 | 817 | self.contentList = contentList 818 | 819 | def findBoundMesh(self, xgDes): 820 | itDg = om.MItDependencyGraph(xgDes.object(), direction=om.MItDependencyGraph.kUpstream) 821 | boundMesh = None 822 | while not itDg.isDone(): 823 | dn = om.MFnDependencyNode(itDg.currentNode()) 824 | if dn.typeName == 'xgmSplineBase': 825 | boundMeshPlug = dn.findPlug('boundMesh', False) 826 | boundMesh = om.MFnMesh( 827 | om.MDagPath.getAPathTo(boundMeshPlug.elementByLogicalIndex(0).source().node())) 828 | break 829 | itDg.next() 830 | return boundMesh 831 | 832 | def setBakeMesh(self, mesh): 833 | if mesh is not None: 834 | self.bakeMesh = mesh 835 | self.MeshName.setText("Mesh: {}".format(mesh.name())) 836 | self.combo.clear() 837 | self.combo.addItems(mesh.getUVSetNames()) 838 | 839 | 840 | SaveXGenWindowInstanceName = '_SaveXGenWindowInstance' 841 | if SaveXGenWindowInstanceName not in globals(): 842 | globals()[SaveXGenWindowInstanceName] = SaveXGenWindow() 843 | globals()[SaveXGenWindowInstanceName].show() 844 | -------------------------------------------------------------------------------- /XGenUEGroomExporter.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import alembic.Abc as abc 3 | import alembic.AbcGeom as abcGeom 4 | import alembic.AbcCoreAbstract as abcA 5 | import maya.OpenMaya as om1 6 | import maya.api.OpenMayaAnim as omAnim 7 | import maya.api.OpenMaya as om 8 | import imath 9 | import array 10 | import zlib 11 | import json 12 | import maya.cmds as cmds 13 | from typing import List 14 | import time 15 | import struct 16 | 17 | # %% 18 | print_debug = False 19 | 20 | 21 | # %% 22 | def list2ImathArray(l: list, _type): 23 | arr = _type(len(l)) 24 | for i in range(len(l)): 25 | arr[i] = l[i] 26 | return arr 27 | 28 | 29 | def floatList2V3fArray(l: list): 30 | arr = imath.V3fArray(len(l) // 3) 31 | for i in range(len(arr)): 32 | arr[i].x = l[i * 3] 33 | arr[i].y = l[i * 3 + 1] 34 | arr[i].z = l[i * 3 + 2] 35 | return arr 36 | 37 | 38 | # %% 39 | def getXgenData(fnDepNode: om.MFnDependencyNode): 40 | splineData: om.MPlug = fnDepNode.findPlug("outSplineData", False) 41 | 42 | handle: om.MDataHandle = splineData.asMObject() 43 | mdata = om.MFnPluginData(handle) 44 | mData = mdata.data() 45 | 46 | rawData = mData.writeBinary() 47 | 48 | def GetBlocks(bype_data): 49 | address = 0 50 | i = 0 51 | blocks = [] 52 | maxIt = 100 53 | while address < len(bype_data) - 1: 54 | size = int.from_bytes(bype_data[address + 8:address + 16], byteorder='little', signed=False) 55 | type_code = int.from_bytes(bype_data[address:address + 4], byteorder='little', signed=False) 56 | blocks.append((address + 16, address + 16 + size, type_code)) 57 | address += size + 16 58 | i += 1 59 | if i > maxIt: 60 | break 61 | return blocks 62 | 63 | dataBlocks = GetBlocks(rawData) 64 | headerBlock = dataBlocks[0] 65 | dataBlocks.pop(0) 66 | 67 | dataJson = json.loads(rawData[headerBlock[0]:headerBlock[1]]) 68 | # print(dataJson) 69 | Header = dataJson['Header'] 70 | 71 | Items = dict() 72 | 73 | def readItems(items): 74 | for k, v in items: 75 | if isinstance(v, int): 76 | group = v >> 32 77 | index = v & 0xFFFFFFFF 78 | addr = (group, index) 79 | if k not in Items: 80 | Items[k] = [addr] 81 | else: 82 | Items[k].append(addr) 83 | 84 | for i in range(len(dataJson['Items'])): 85 | readItems(dataJson['Items'][i].items()) 86 | for i in range(len(dataJson['RefMeshArray'])): 87 | readItems(dataJson['RefMeshArray'][i].items()) 88 | 89 | # print(Items) 90 | decompressedData = dict() 91 | 92 | def decompressData(group, index): 93 | if group not in decompressedData: 94 | if Header['GroupBase64']: 95 | raise Exception("我还没有碰到Base64的情况,请提醒我更新代码") 96 | if Header['GroupDeflate']: 97 | validData = zlib.decompress(rawData[dataBlocks[group][0] + 32:]) 98 | else: 99 | validData = rawData[dataBlocks[group][0]:dataBlocks[group][1]] 100 | decompressedData[group] = validData 101 | else: 102 | validData = decompressedData[group] 103 | blocks = GetBlocks(validData) 104 | return validData[blocks[index][0]:blocks[index][1]] 105 | 106 | PrimitiveInfosList = [] 107 | PositionsDataList = [] 108 | WidthsDataList = [] 109 | for k, v in Items.items(): 110 | # print(k, len(v)) 111 | if k == 'PrimitiveInfos': 112 | dtype_format = ' 1: 217 | knotsLength = len(knotsArray) 218 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 219 | knotsArray[0] == knotsArray[1]): 220 | knots.append(float(knotsArray[0])) 221 | else: 222 | knots.append(float(2 * knotsArray[0] - knotsArray[1])) 223 | 224 | for j in range(knotsLength): 225 | knots.append(float(knotsArray[j])) 226 | 227 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 228 | knotsArray[knotsLength - 1] == knotsArray[knotsLength - 2]): 229 | knots.append(float(knotsArray[knotsLength - 1])) 230 | else: 231 | knots.append(float(2 * knotsArray[knotsLength - 1] - knotsArray[knotsLength - 2])) 232 | samp.setCurvesNumVertices(nVertices) 233 | samp.setPositions(floatList2V3fArray(pointslist)) 234 | samp.setOrders(list2ImathArray(orders, imath.UnsignedCharArray)) 235 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 236 | 237 | # widths = list2ImathArray([0.1], imath.FloatArray) 238 | # widths = abc.Float32TPTraits() 239 | # widths = abcGeom.OFloatGeomParamSample(widths, abcGeom.GeometryScope.kConstantScope) 240 | # samp.setWidths(widths) 241 | self.schema.set(samp) 242 | 243 | def write_frame(self): 244 | numCurves = len(self.curves) 245 | if numCurves == 0: 246 | return 247 | curve = om.MFnNurbsCurve(self.curves[0]) 248 | 249 | samp = abcGeom.OCurvesSchemaSample() 250 | samp.setBasis(self.firstSamp.getBasis()) 251 | samp.setWrap(self.firstSamp.getWrap()) 252 | samp.setType(self.firstSamp.getType()) 253 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 254 | samp.setOrders(self.firstSamp.getOrders()) 255 | samp.setKnots(self.firstSamp.getKnots()) 256 | 257 | pointslist = [] 258 | for i in range(numCurves): 259 | curve = curve.setObject(self.curves[i]) 260 | numCVs = curve.numCVs 261 | cvArray = curve.cvPositions() 262 | for j in range(numCVs): 263 | pointslist.append(cvArray[j].x) 264 | pointslist.append(cvArray[j].y) 265 | pointslist.append(cvArray[j].z) 266 | 267 | samp.setPositions(floatList2V3fArray(pointslist)) 268 | 269 | self.schema.set(samp) 270 | 271 | def bake_uv(self, bakeMesh: om.MFnMesh, uv_set: str = None): 272 | if self.hairRootList is None: 273 | return 274 | if bakeMesh is None: 275 | return 276 | if uv_set is None: 277 | uv_set = bakeMesh.currentUVSetName() 278 | elif uv_set not in bakeMesh.getUVSetNames(): 279 | raise Exception(f'Invalid UV Set : {uv_set}') 280 | 281 | uvs = imath.V2fArray(len(self.hairRootList)) 282 | for i, hairRoot in enumerate(self.hairRootList): 283 | res = bakeMesh.getUVAtPoint(hairRoot, om.MSpace.kWorld, uvSet=uv_set) 284 | uvs[i].x = res[0] 285 | uvs[i].y = res[1] 286 | 287 | cp: abc.OCompoundProperty = self.schema.getArbGeomParams() 288 | uv_prop = abc.OV2fArrayProperty(cp, "groom_root_uv") 289 | uv_prop.setValue(uvs) 290 | 291 | 292 | class XGenProxy(CurvesProxy): 293 | def __init__(self, curveObj: abcGeom.OCurves, fnDepNode: om.MFnDependencyNode, needBakeUV=False, animation=False): 294 | super().__init__(curveObj, fnDepNode, needBakeUV, animation) 295 | 296 | def write_first_frame(self): 297 | if print_debug: 298 | startTime = time.time() 299 | PrimitiveInfosList, PositionsDataList, WidthsDataList = getXgenData(self.fnDepNode) 300 | if print_debug: 301 | print("getXgenData: %.4f" % (time.time() - startTime)) 302 | startTime = time.time() 303 | numCurves = 0 304 | numCVs = 0 305 | for i, PrimitiveInfos in enumerate(PrimitiveInfosList): 306 | numCurves += len(PrimitiveInfos) 307 | for PrimitiveInfo in PrimitiveInfos: 308 | numCVs += PrimitiveInfo[1] 309 | 310 | orders = imath.UnsignedCharArray(numCurves) 311 | nVertices = imath.IntArray(numCurves) 312 | cp: abc.OCompoundProperty = self.schema.getArbGeomParams() 313 | 314 | samp = self.firstSamp 315 | samp.setBasis(abcGeom.BasisType.kBsplineBasis) 316 | samp.setWrap(abcGeom.CurvePeriodicity.kNonPeriodic) 317 | samp.setType(abcGeom.CurveType.kCubic) 318 | 319 | degree = 3 320 | pointArray = imath.V3fArray(numCVs) 321 | widthArray = imath.FloatArray(numCVs) 322 | if self.needBakeUV: 323 | self.hairRootList = [] 324 | knots = [] 325 | 326 | curveIndex = 0 327 | cvIndex = 0 328 | 329 | for j in range(len(PrimitiveInfosList)): 330 | PrimitiveInfos = PrimitiveInfosList[j] 331 | posData = PositionsDataList[j] 332 | widthData = WidthsDataList[j] 333 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 334 | offset = PrimitiveInfo[0] 335 | length = int(PrimitiveInfo[1]) 336 | if length < 2: 337 | continue 338 | startAddr = offset * 3 339 | for k in range(length): 340 | pointArray[cvIndex].x = posData[startAddr] 341 | pointArray[cvIndex].y = posData[startAddr + 1] 342 | pointArray[cvIndex].z = posData[startAddr + 2] 343 | if k == 0 and self.needBakeUV: 344 | self.hairRootList.append(om.MPoint(pointArray[cvIndex])) 345 | widthArray[cvIndex] = widthData[offset + k] 346 | startAddr += 3 347 | cvIndex += 1 348 | 349 | orders[curveIndex] = degree + 1 350 | nVertices[curveIndex] = length 351 | 352 | knotsInsideNum = length - degree + 1 353 | knotsList = [*([0] * degree), *list(range(knotsInsideNum)), 354 | *([knotsInsideNum - 1] * degree )] 355 | knots += knotsList 356 | curveIndex += 1 357 | 358 | samp.setCurvesNumVertices(nVertices) 359 | samp.setPositions(pointArray) 360 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 361 | samp.setOrders(orders) 362 | 363 | # bake vertex color example 364 | # cvColor = abcGeom.OC3fGeomParam(cp, "groom_color", False, abcGeom.GeometryScope.kVertexScope, 1) 365 | # cvColorArray = imath.C3fArray(len(pointslist) // 3) 366 | # i = 0 367 | # color1 = imath.Color3f((0, 1, 1)); 368 | # color2 = imath.Color3f((1, 0, 1)) 369 | # for _ in range(len(nVertices)): 370 | # length = nVertices[_] 371 | # for j in range(length): 372 | # t = (j / (length - 1)) 373 | # cvColorArray[i] = color1 * (1 - t) + color2 * t 374 | # i += 1 375 | # cvColorArray = abcGeom.OC3fGeomParamSample(cvColorArray, abcGeom.GeometryScope.kVertexScope) 376 | # cvColor.set(cvColorArray) 377 | 378 | # write width 379 | widths = abcGeom.OFloatGeomParamSample(widthArray, abcGeom.GeometryScope.kVertexScope) 380 | samp.setWidths(widths) 381 | self.schema.set(samp) 382 | 383 | if print_debug: 384 | print("write_first_frame: %.4f" % (time.time() - startTime)) 385 | 386 | def write_frame(self): 387 | if print_debug: 388 | startTime = time.time() 389 | PrimitiveInfosList, PositionsDataList, WidthsDataList = getXgenData(self.fnDepNode) 390 | numCurves = 0 391 | numCVs = 0 392 | for i, PrimitiveInfos in enumerate(PrimitiveInfosList): 393 | numCurves += len(PrimitiveInfos) 394 | for PrimitiveInfo in PrimitiveInfos: 395 | numCVs += PrimitiveInfo[1] 396 | 397 | cp: abc.OCompoundProperty = self.schema.getArbGeomParams() 398 | 399 | samp = abcGeom.OCurvesSchemaSample() 400 | samp.setBasis(self.firstSamp.getBasis()) 401 | samp.setWrap(self.firstSamp.getWrap()) 402 | samp.setType(self.firstSamp.getType()) 403 | 404 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 405 | samp.setKnots(self.firstSamp.getKnots()) 406 | samp.setOrders(self.firstSamp.getOrders()) 407 | samp.setWidths(self.firstSamp.getWidths()) 408 | 409 | pointArray = imath.V3fArray(numCVs) 410 | 411 | curveIndex = 0 412 | cvIndex = 0 413 | for j in range(len(PrimitiveInfosList)): 414 | PrimitiveInfos = PrimitiveInfosList[j] 415 | posData = PositionsDataList[j] 416 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 417 | offset = PrimitiveInfo[0] 418 | length = int(PrimitiveInfo[1]) 419 | if length < 2: 420 | continue 421 | startAddr = offset * 3 422 | for k in range(length): 423 | pointArray[cvIndex].x = posData[startAddr] 424 | pointArray[cvIndex].y = posData[startAddr + 1] 425 | pointArray[cvIndex].z = posData[startAddr + 2] 426 | startAddr += 3 427 | cvIndex += 1 428 | 429 | curveIndex += 1 430 | 431 | samp.setPositions(pointArray) 432 | 433 | self.schema.set(samp) 434 | if print_debug: 435 | print("write_frame: %.4f" % (time.time() - startTime)) 436 | 437 | 438 | # %% 439 | try: 440 | from PySide6 import QtCore, QtWidgets, QtGui 441 | import shiboken6 as shiboken 442 | except: 443 | from PySide2 import QtCore, QtWidgets, QtGui 444 | import shiboken2 as shiboken 445 | 446 | import maya.OpenMayaUI as om1ui 447 | 448 | 449 | def mayaWindow(): 450 | main_window_ptr = om1ui.MQtUtil.mainWindow() 451 | return shiboken.wrapInstance(int(main_window_ptr), QtWidgets.QWidget) 452 | 453 | 454 | # %% 455 | class SaveXGenWindow(QtWidgets.QDialog): 456 | class Content: 457 | def __init__(self, fnDepNode, showName, Type, groupName, isGuide, bakeUV, animation, export): 458 | self.showName = showName 459 | self.fnDepNode = fnDepNode 460 | self.Type = Type 461 | self.groupName = QtWidgets.QLineEdit() 462 | self.groupName.setText(groupName) 463 | self.isGuide = QtWidgets.QCheckBox() 464 | self.isGuide.setChecked(isGuide) 465 | self.bakeUV = QtWidgets.QCheckBox() 466 | self.bakeUV.setChecked(bakeUV) 467 | self.animation = QtWidgets.QCheckBox() 468 | self.animation.setChecked(animation) 469 | self.export = QtWidgets.QCheckBox() 470 | self.export.setChecked(export) 471 | 472 | curveType = "curve" 473 | xgenType = "xgen" 474 | 475 | def __init__(self, parent=mayaWindow()): 476 | super(SaveXGenWindow, self).__init__(parent) 477 | self.contentList: List[SaveXGenWindow.Content] = [] 478 | self.save_path = '.' 479 | self.bakeMesh = None 480 | self.setWindowTitle("Export XGen to UE Groom") 481 | self.setGeometry(400, 400, 850, 450) 482 | self.buildUI() 483 | 484 | def showAbout(self): 485 | QtWidgets.QMessageBox.about(self, "Export XGen to UE Groom", 486 | "A small tool to export XGen to UE Groom, by PDE26jjk. Link: https://github.com/PDE26jjk/XGenUEGroomExporter") 487 | 488 | def createFrame(self, labelText): 489 | try: 490 | frame = om1ui.MQtUtil.findControl( 491 | cmds.frameLayout(label=labelText, collapsable=True, collapse=True, manage=True)) 492 | frame: QtWidgets.QWidget = shiboken.wrapInstance(int(frame), QtWidgets.QWidget) 493 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 494 | frameLayout: QtWidgets.QLayout = frame.children()[2].children()[0] 495 | except: 496 | frame = QtWidgets.QFrame(self) 497 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 498 | frameLayout = QtWidgets.QVBoxLayout(frame) 499 | frame.children().append(frameLayout) 500 | return frame, frameLayout 501 | 502 | def buildUI(self): 503 | main_layout = QtWidgets.QVBoxLayout() 504 | 505 | menu_bar = QtWidgets.QMenuBar(self) 506 | menu_bar.addMenu("Help").addAction("About", self.showAbout) 507 | main_layout.setMenuBar(menu_bar) 508 | 509 | label1 = QtWidgets.QLabel("Please select Curves and Interactive XGen") # 请选择曲线和交互式XGen 510 | label1.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 511 | hBox = QtWidgets.QHBoxLayout() 512 | hBox.setContentsMargins(10, 4, 10, 4) 513 | hBox.addWidget(label1) 514 | 515 | self.fillWithSelectList_button = QtWidgets.QPushButton("Refresh selected") # 刷新选择 516 | self.fillWithSelectList_button.clicked.connect(self.fillWithSelectList) 517 | self.fillWithSelectList_button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 518 | # main_layout.addWidget(self.fillWithSelectList_button) 519 | hBox.addStretch(1) 520 | hBox.addWidget(self.fillWithSelectList_button) 521 | main_layout.addLayout(hBox) 522 | 523 | self.table = QtWidgets.QTableWidget(self) 524 | self.table.setColumnCount(8) # 设置列数 525 | self.table.setHorizontalHeaderLabels(["", "Name", "Type", "Group name", "Is guide", "Bake UV", "Animation", ""]) 526 | self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) 527 | self.table.setColumnWidth(0, 40) 528 | self.table.horizontalHeader().setSectionResizeMode(2, QtWidgets.QHeaderView.ResizeToContents) 529 | self.table.setColumnWidth(4, 140) 530 | self.table.horizontalHeader().setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) 531 | self.table.setColumnWidth(5, 140) 532 | self.table.horizontalHeader().setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) 533 | self.table.setColumnWidth(6, 140) 534 | self.table.horizontalHeader().setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) 535 | self.table.horizontalHeader().setStretchLastSection(True) 536 | 537 | self.table.setStyleSheet(""" 538 | QTableView::item 539 | { 540 | border: 0px; 541 | padding: 5px; 542 | background-color: rgb(68, 68, 68); 543 | } 544 | QTableView::item QCheckBox { 545 | padding-left:60px; 546 | } 547 | """) 548 | 549 | self.table.clearContents() 550 | self.table.setRowCount(0) 551 | 552 | self.Bakeframe, frameLayout = self.createFrame(labelText="Bake UV") 553 | 554 | self.MeshName = QtWidgets.QLabel("Mesh : ---") 555 | hBox = QtWidgets.QHBoxLayout() 556 | hBox.setContentsMargins(10, 10, 10, 10) 557 | hBox.addWidget(self.MeshName) 558 | hBox2 = QtWidgets.QHBoxLayout() 559 | label = QtWidgets.QLabel("UV Set : ") 560 | hBox2.addWidget(label) 561 | self.combo = QtWidgets.QComboBox() 562 | self.combo.addItem(" --- ") 563 | 564 | self.uvSetStr = QtWidgets.QLabel("Selected: None") 565 | 566 | self.combo.currentIndexChanged.connect(self.update_label) 567 | hBox2.addWidget(self.combo) 568 | hBox.addStretch(2) 569 | hBox.addLayout(hBox2) 570 | hBox.addStretch(1) 571 | 572 | frameLayout.addLayout(hBox) 573 | 574 | self.button3 = QtWidgets.QPushButton("Pick other mesh", self) 575 | self.button3.clicked.connect(self.pick_mesh) 576 | self.button3.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 577 | frameLayout.addWidget(self.button3) 578 | 579 | self.separator = QtWidgets.QFrame(self) 580 | self.separator.setFrameShape(QtWidgets.QFrame.HLine) 581 | self.separator.setFrameShadow(QtWidgets.QFrame.Sunken) 582 | 583 | self.AnimationFrame, frameLayout = self.createFrame(labelText="Animation") 584 | 585 | validator = QtGui.QIntValidator() 586 | validator.setRange(0, 99999) 587 | self.startFrame = QtWidgets.QLineEdit() 588 | self.startFrame.setMaximumWidth(60) 589 | self.startFrame.setValidator(validator) 590 | self.startFrame.setText(str(0)) 591 | self.endFrame = QtWidgets.QLineEdit() 592 | self.endFrame.setMaximumWidth(60) 593 | self.endFrame.setValidator(validator) 594 | self.endFrame.setText(str(0)) 595 | self.preroll = QtWidgets.QCheckBox("Preroll") 596 | 597 | frameLayout.setContentsMargins(10, 10, 10, 10) 598 | hBox = QtWidgets.QHBoxLayout() 599 | hBox.addWidget(QtWidgets.QLabel("Frame Range : ")) 600 | hBox.addWidget(self.startFrame) 601 | hBox.addWidget(QtWidgets.QLabel(" ~ ")) 602 | hBox.addWidget(self.endFrame) 603 | hBox.addStretch(1) 604 | hBox2 = QtWidgets.QHBoxLayout() 605 | hBox2.addWidget(self.preroll) 606 | hBox2.addStretch(1) 607 | 608 | frameLayout.addLayout(hBox) 609 | frameLayout.addLayout(hBox2) 610 | 611 | self.SettingFrame, frameLayout = self.createFrame(labelText="Setting") 612 | 613 | frameLayout.setContentsMargins(10, 10, 10, 10) 614 | hBox = QtWidgets.QHBoxLayout() 615 | self.createGroupId_cb = QtWidgets.QCheckBox("Create group id") 616 | self.createGroupId_cb.setChecked(True) 617 | hBox.addWidget(self.createGroupId_cb) 618 | 619 | frameLayout.addLayout(hBox) 620 | 621 | self.save_button = QtWidgets.QPushButton("Save Alembic File", self) 622 | self.save_button.clicked.connect(self.save_abc) 623 | 624 | self.cancel_button = QtWidgets.QPushButton("Close", self) # 关闭 625 | self.cancel_button.clicked.connect(self.close) # 关闭窗口 626 | 627 | button_layout = QtWidgets.QHBoxLayout() 628 | button_layout.addWidget(self.save_button) 629 | button_layout.addWidget(self.cancel_button) 630 | 631 | main_layout.addWidget(self.table) 632 | main_layout.addWidget(self.Bakeframe) 633 | main_layout.addWidget(self.AnimationFrame) 634 | main_layout.addWidget(self.SettingFrame) 635 | main_layout.addWidget(self.separator) 636 | main_layout.addLayout(button_layout) 637 | 638 | self.setLayout(main_layout) 639 | 640 | def pick_mesh(self): 641 | selectionList = om.MGlobal.getActiveSelectionList() 642 | if selectionList.length() > 0: 643 | dag_path = selectionList.getDagPath(0) 644 | fnDepNode = om.MFnDependencyNode(dag_path.node()) 645 | itDag = om.MItDag() 646 | # find mesh 647 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kMesh) 648 | while not itDag.isDone(): 649 | meshPath = om.MDagPath.getAPathTo(itDag.currentItem()) 650 | mesh = om.MFnMesh(meshPath) 651 | self.setBakeMesh(mesh) 652 | break 653 | 654 | def update_label(self): 655 | selected_option = self.combo.currentText() 656 | self.uvSetStr.setText(selected_option) 657 | 658 | def save_abc(self): 659 | if len(self.contentList) == 0: 660 | print("No content") 661 | return 662 | file_path = cmds.fileDialog2( 663 | # dialogStyle=2, 664 | caption="Save as Alembic File", 665 | fileMode=0, 666 | okCaption="save", 667 | # defaultExtension='abc', 668 | startingDirectory=self.save_path, 669 | ff='Alembic Files (*.abc);;All Files (*)' 670 | ) 671 | if file_path: 672 | self.save_path = file_path[0] 673 | else: 674 | return 675 | startTime = time.time() 676 | oldCurTime = omAnim.MAnimControl.currentTime() 677 | archive = abc.OArchive(file_path[0]) 678 | 679 | anyAnimation = False 680 | for item in self.contentList: 681 | hasAnimation = item.animation.isChecked() 682 | if hasAnimation: 683 | anyAnimation = True 684 | break 685 | if anyAnimation: 686 | frameRange = [int(self.startFrame.text()), int(self.endFrame.text())] 687 | if (frameRange[0] > frameRange[1] 688 | or frameRange[0] < omAnim.MAnimControl.minTime().value 689 | or frameRange[1] > omAnim.MAnimControl.maxTime().value): 690 | raise ValueError("Frame out of range.") 691 | # frameRange[0] = int(max(frameRange[0], omAnim.MAnimControl.minTime().value)) 692 | # frameRange[1] = int(min(frameRange[1], omAnim.MAnimControl.maxTime().value)) 693 | 694 | sec = om.MTime(1, om.MTime.kSeconds) 695 | spf = 1.0 / sec.asUnits(om.MTime.uiUnit()) 696 | timeSampling = abcA.TimeSampling(spf, spf * frameRange[0]) 697 | 698 | timeIndex = archive.addTimeSampling(timeSampling) 699 | 700 | proxyList: List[CurvesProxy] = [] 701 | for item in self.contentList: 702 | if not item.export: 703 | continue 704 | fnDepNode = item.fnDepNode 705 | needBakeUV = item.bakeUV.isChecked() 706 | hasAnimation = item.animation.isChecked() 707 | if hasAnimation: 708 | curveObj = abcGeom.OCurves(archive.getTop(), fnDepNode.name(), timeIndex) 709 | else: 710 | curveObj = abcGeom.OCurves(archive.getTop(), fnDepNode.name()) 711 | 712 | if item.Type == SaveXGenWindow.xgenType: 713 | proxy = XGenProxy(curveObj, fnDepNode, needBakeUV, hasAnimation) 714 | elif item.Type == SaveXGenWindow.curveType: 715 | proxy = CurvesProxy(curveObj, fnDepNode, needBakeUV, hasAnimation) 716 | else: 717 | continue 718 | proxyList.append(proxy) 719 | proxy.write_group_name(item.groupName.text()) 720 | proxy.write_is_guide(item.isGuide.isChecked()) 721 | 722 | if len(proxyList) == 0: 723 | print("No content") 724 | return 725 | 726 | if self.createGroupId_cb.isChecked(): 727 | groupIds = dict() 728 | currentId = 0 729 | for proxy in proxyList: 730 | if proxy.groupName not in groupIds: 731 | groupIds[proxy.groupName] = currentId 732 | currentId += 1 733 | proxy.write_group_id(groupIds[proxy.groupName]) 734 | 735 | if anyAnimation: 736 | if self.preroll.isChecked(): 737 | for frame in range(int(omAnim.MAnimControl.minTime().value), frameRange[0]): 738 | om.MGlobal.viewFrame(frame) 739 | for frame in range(frameRange[0], frameRange[1] + 1): 740 | om.MGlobal.viewFrame(frame) 741 | for item in proxyList: 742 | if frame == frameRange[0]: 743 | item.write_first_frame() 744 | elif item.animation: 745 | item.write_frame() 746 | omAnim.MAnimControl.setCurrentTime(oldCurTime) 747 | else: 748 | for item in proxyList: 749 | item.write_first_frame() 750 | for item in proxyList: 751 | item.bake_uv(self.bakeMesh, self.uvSetStr.text()) 752 | print("Data has been saved in %s, it took %.2f seconds." % (file_path[0], time.time() - startTime)) 753 | 754 | return file_path[0] 755 | 756 | def fillWithSelectList(self): 757 | self.contentList = [] 758 | selectionList = om.MGlobal.getActiveSelectionList() 759 | contentList = [] 760 | for i in range(selectionList.length()): 761 | dag_path = selectionList.getDagPath(i) 762 | fnDepNode = om.MFnDependencyNode(dag_path.node()) 763 | itDag = om.MItDag() 764 | # find xgen description 765 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kNamedObject) 766 | xgDes = None 767 | while not itDag.isDone(): 768 | dn = om.MFnDependencyNode(itDag.currentItem()) 769 | if dn.typeName == 'xgmSplineDescription': 770 | xgDes = dn 771 | break 772 | itDag.next() 773 | if xgDes is not None: 774 | contentList.append( 775 | SaveXGenWindow.Content(xgDes, fnDepNode.name(), SaveXGenWindow.xgenType, fnDepNode.name(), False, 776 | False, False, True)) 777 | boundMesh = self.findBoundMesh(xgDes) 778 | if boundMesh is not None: 779 | self.setBakeMesh(boundMesh) 780 | continue 781 | # find curve 782 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kCurve) 783 | isCurve = False 784 | while not itDag.isDone(): 785 | isCurve = True 786 | break 787 | if isCurve: 788 | groupNameStr: str = fnDepNode.name() 789 | suffix = "_guide" 790 | if groupNameStr.endswith(suffix): 791 | groupNameStr = groupNameStr[:-len(suffix)] 792 | contentList.append( 793 | SaveXGenWindow.Content(fnDepNode, fnDepNode.name(), SaveXGenWindow.curveType, groupNameStr, True, 794 | False, False, True)) 795 | 796 | self.table.setRowCount(len(contentList)) 797 | for row in range(len(contentList)): 798 | self.table.setCellWidget(row, 0, contentList[row].export) 799 | contentList[row].export.setStyleSheet("padding-left:8px") 800 | item = QtWidgets.QTableWidgetItem(contentList[row].showName) 801 | # item.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 802 | item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) 803 | self.table.setItem(row, 1, item) 804 | 805 | item = QtWidgets.QTableWidgetItem(contentList[row].Type) 806 | # item.setTextAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter) 807 | item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) 808 | self.table.setItem(row, 2, item) 809 | 810 | self.table.setCellWidget(row, 3, contentList[row].groupName) 811 | self.table.setCellWidget(row, 4, contentList[row].isGuide) 812 | self.table.setCellWidget(row, 5, contentList[row].bakeUV) 813 | self.table.setCellWidget(row, 6, contentList[row].animation) 814 | 815 | self.contentList = contentList 816 | 817 | def findBoundMesh(self, xgDes): 818 | itDg = om.MItDependencyGraph(xgDes.object(), direction=om.MItDependencyGraph.kUpstream) 819 | boundMesh = None 820 | while not itDg.isDone(): 821 | dn = om.MFnDependencyNode(itDg.currentNode()) 822 | if dn.typeName == 'xgmSplineBase': 823 | boundMeshPlug: om.MPlug = dn.findPlug('boundMesh', False) 824 | boundMesh = om.MFnMesh( 825 | om.MDagPath.getAPathTo(boundMeshPlug.elementByLogicalIndex(0).source().node())) 826 | break 827 | itDg.next() 828 | return boundMesh 829 | 830 | def setBakeMesh(self, mesh: om.MFnMesh): 831 | if mesh is not None: 832 | self.bakeMesh = mesh 833 | self.MeshName.setText(f"Mesh: {mesh.name()}") 834 | self.combo.clear() 835 | self.combo.addItems(mesh.getUVSetNames()) 836 | 837 | 838 | SaveXGenWindowInstanceName = '_SaveXGenWindowInstance' 839 | if SaveXGenWindowInstanceName not in globals(): 840 | globals()[SaveXGenWindowInstanceName] = SaveXGenWindow() 841 | globals()[SaveXGenWindowInstanceName].show() 842 | -------------------------------------------------------------------------------- /XGenDescriptionUEGroomExporter_py2.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import alembic.Abc as abc 3 | import alembic.AbcGeom as abcGeom 4 | import alembic.AbcCoreAbstract as abcA 5 | import maya.OpenMaya as om1 6 | import maya.api.OpenMayaAnim as omAnim 7 | import maya.api.OpenMaya as om 8 | 9 | om2 = om 10 | import imath 11 | import array 12 | import zlib 13 | import json 14 | import maya.cmds as cmds 15 | import time 16 | import struct 17 | import uuid 18 | import xgenm as xg 19 | import os 20 | import itertools 21 | 22 | _XGenExporterVersion = "1.09" 23 | print_debug = False 24 | 25 | 26 | # %% 27 | def list2ImathArray(l, _type): 28 | arr = _type(len(l)) 29 | for i in range(len(l)): 30 | arr[i] = l[i] 31 | return arr 32 | 33 | 34 | def floatList2V3fArray(l): 35 | arr = imath.V3fArray(len(l) // 3) 36 | for i in range(len(arr)): 37 | arr[i].x = l[i * 3] 38 | arr[i].y = l[i * 3 + 1] 39 | arr[i].z = l[i * 3 + 2] 40 | return arr 41 | 42 | 43 | # %% 44 | def getXgenData(fnDepNode, keys): 45 | splineData = fnDepNode.findPlug("outSplineData", False) 46 | 47 | handle = splineData.asMObject() 48 | mdata = om2.MFnPluginData(handle) 49 | mData = mdata.data() 50 | 51 | rawData = mData.writeBinary() 52 | 53 | def GetBlocks(bype_data): 54 | address = 0 55 | i = 0 56 | blocks = [] 57 | maxIt = 100 58 | while address < len(bype_data) - 1: 59 | size = struct.unpack(' maxIt: 65 | break 66 | return blocks 67 | 68 | dataBlocks = GetBlocks(rawData) 69 | headerBlock = dataBlocks[0] 70 | dataBlocks.pop(0) 71 | 72 | dataString = str(rawData[headerBlock[0]:headerBlock[1]]) 73 | dataJson = json.loads(dataString) 74 | # print(dataJson) 75 | Header = dataJson['Header'] 76 | 77 | Items = dict() 78 | 79 | def readItems(items): 80 | for k, v in items: 81 | if isinstance(v, (int, long)): 82 | group = v >> 32 83 | index = v & 0xFFFFFFFF 84 | addr = (group, index) 85 | if k not in Items: 86 | Items[k] = [addr] 87 | else: 88 | Items[k].append(addr) 89 | 90 | for i in range(len(dataJson['Items'])): 91 | readItems(dataJson['Items'][i].items()) 92 | for i in range(len(dataJson['RefMeshArray'])): 93 | readItems(dataJson['RefMeshArray'][i].items()) 94 | 95 | # print(Items) 96 | decompressedData = dict() 97 | 98 | def decompressData(group, index): 99 | if group not in decompressedData: 100 | if Header['GroupBase64']: 101 | raise Exception("我还没有碰到Base64的情况,请提醒我更新代码") 102 | if Header['GroupDeflate']: 103 | validData = zlib.decompress(str(rawData[dataBlocks[group][0] + 32:])) 104 | else: 105 | validData = rawData[dataBlocks[group][0]:dataBlocks[group][1]] 106 | decompressedData[group] = validData 107 | else: 108 | validData = decompressedData[group] 109 | blocks = GetBlocks(validData) 110 | return validData[blocks[index][0]:blocks[index][1]] 111 | 112 | outputs = {key: [] for key in keys} 113 | for k, v in Items.items(): 114 | if k not in outputs: 115 | continue 116 | if k == 'PrimitiveInfos': 117 | dtype_format = ' 1: 235 | knotsLength = len(knotsArray) 236 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 237 | knotsArray[0] == knotsArray[1]): 238 | knots.append(float(knotsArray[0])) 239 | else: 240 | knots.append(float(2 * knotsArray[0] - knotsArray[1])) 241 | 242 | for j in range(knotsLength): 243 | knots.append(float(knotsArray[j])) 244 | 245 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 246 | knotsArray[knotsLength - 1] == knotsArray[knotsLength - 2]): 247 | knots.append(float(knotsArray[knotsLength - 1])) 248 | else: 249 | knots.append(float(2 * knotsArray[knotsLength - 1] - knotsArray[knotsLength - 2])) 250 | samp.setCurvesNumVertices(nVertices) 251 | samp.setPositions(floatList2V3fArray(pointslist)) 252 | samp.setOrders(list2ImathArray(orders, imath.UnsignedCharArray)) 253 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 254 | 255 | # widths = list2ImathArray([0.1], imath.FloatArray) 256 | # widths = abc.Float32TPTraits() 257 | # widths = abcGeom.OFloatGeomParamSample(widths, abcGeom.GeometryScope.kConstantScope) 258 | # samp.setWidths(widths) 259 | self.schema.set(samp) 260 | 261 | def write_frame(self): 262 | numCurves = len(self.curves) 263 | if numCurves == 0: 264 | return 265 | curve = om2.MFnNurbsCurve(self.curves[0]) 266 | 267 | samp = abcGeom.OCurvesSchemaSample() 268 | samp.setBasis(self.firstSamp.getBasis()) 269 | samp.setWrap(self.firstSamp.getWrap()) 270 | samp.setType(self.firstSamp.getType()) 271 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 272 | samp.setOrders(self.firstSamp.getOrders()) 273 | samp.setKnots(self.firstSamp.getKnots()) 274 | 275 | pointslist = [] 276 | for i in range(numCurves): 277 | curve = curve.setObject(self.curves[i]) 278 | numCVs = curve.numCVs 279 | cvArray = curve.cvPositions() 280 | for j in range(numCVs): 281 | pointslist.append(cvArray[j].x) 282 | pointslist.append(cvArray[j].y) 283 | pointslist.append(cvArray[j].z) 284 | 285 | samp.setPositions(floatList2V3fArray(pointslist)) 286 | 287 | self.schema.set(samp) 288 | 289 | def bake_uv(self, bakeMesh, uv_set=None): 290 | if not self.needBakeUV or self.hairRootList is None: 291 | return 292 | if bakeMesh is None: 293 | return 294 | if uv_set is None: 295 | uv_set = bakeMesh.currentUVSetName() 296 | elif uv_set not in bakeMesh.getUVSetNames(): 297 | raise Exception('Invalid UV Set : {}'.format(uv_set)) 298 | 299 | uvs = imath.V2fArray(len(self.hairRootList)) 300 | for i, hairRoot in enumerate(self.hairRootList): 301 | res = bakeMesh.getUVAtPoint(hairRoot, om2.MSpace.kWorld, uvSet=uv_set) 302 | uvs[i].x = res[0] 303 | uvs[i].y = res[1] 304 | 305 | self.write_param('groom_root_uv', AbcType.vector2f, uvs) 306 | 307 | 308 | # %% 309 | try: 310 | from PySide6 import QtCore, QtWidgets, QtGui 311 | import shiboken6 as shiboken 312 | except: 313 | from PySide2 import QtCore, QtWidgets, QtGui 314 | import shiboken2 as shiboken 315 | 316 | import maya.OpenMayaUI as om1ui 317 | 318 | 319 | def mayaWindow(): 320 | main_window_ptr = om1ui.MQtUtil.mainWindow() 321 | return shiboken.wrapInstance(int(main_window_ptr), QtWidgets.QWidget) 322 | 323 | 324 | class FileSelectorWidget(QtWidgets.QWidget): 325 | def __init__(self, callback): 326 | super(FileSelectorWidget, self).__init__() 327 | self.callback = callback 328 | self.setup_ui() 329 | 330 | def setup_ui(self): 331 | self.layout = QtWidgets.QHBoxLayout(self) 332 | self.file_line_edit = QtWidgets.QLineEdit(self) 333 | self.layout.addWidget(self.file_line_edit) 334 | self.browse_button = QtWidgets.QPushButton("Browse", self) 335 | self.layout.addWidget(self.browse_button) 336 | self.browse_button.clicked.connect(self.browse_file) 337 | self.file_line_edit.textChanged.connect(self.callback) 338 | 339 | def browse_file(self): 340 | file_path = cmds.fileDialog2(fileMode=1, caption="Select a File") 341 | if file_path: 342 | self.file_line_edit.setText(file_path[0]) 343 | 344 | def set_file_path(self, path): 345 | return self.file_line_edit.setText(path) 346 | 347 | def get_file_path(self): 348 | return self.file_line_edit.text() 349 | 350 | 351 | # %% 352 | SaveXGenDesWindowParentName = "_saveXGenDesWindow" 353 | 354 | 355 | def getSaveXGenDesWindowParent(): 356 | sel = om2.MSelectionList() 357 | try: 358 | sel.add(SaveXGenDesWindowParentName) 359 | except: 360 | trans = om2.MFnTransform() 361 | trans.create() 362 | trans.setName(SaveXGenDesWindowParentName) 363 | sel.add(SaveXGenDesWindowParentName) 364 | return sel.getDagPath(0) 365 | 366 | 367 | def deleteSaveXGenDesWindowParent(): 368 | if cmds.objExists(SaveXGenDesWindowParentName): 369 | cmds.delete(SaveXGenDesWindowParentName) 370 | 371 | 372 | # %% 373 | 374 | 375 | def getExpressionPath(expr, pal_path, des_path, fx_name): 376 | expr = expr.replace('${DESC}', xg.descriptionPath(pal_path, des_path)).replace('${FXMODULE}', fx_name) 377 | ptex_path = None 378 | if os.path.isdir(expr): 379 | for f in os.listdir(expr): 380 | if f.endswith('.ptx'): 381 | ptex_path = f 382 | 383 | if ptex_path is None: 384 | return "" 385 | return os.path.normpath(os.path.join(expr, ptex_path)) 386 | 387 | 388 | def getClumpingPtexPath(dn): 389 | if not dn.object().hasFn(om2.MFn.kTransform): 390 | des_obj = om2.MFnDagNode(dn.object()).parent(0) 391 | else: 392 | des_obj = dn.object() 393 | des_path = str(om2.MDagPath.getAPathTo(des_obj)) 394 | pal_path = str(om2.MDagPath.getAPathTo(om2.MFnDagNode(des_obj).parent(0))) 395 | clumping = None 396 | for fx in xg.fxModules(pal_path, des_path): 397 | if fx.startswith('Clumping'): 398 | clumping = fx 399 | break 400 | if clumping is None: 401 | return "" 402 | expr = xg.getAttr("mapDir", pal_path, des_path, clumping) 403 | return getExpressionPath(expr, pal_path, des_path, clumping) 404 | 405 | 406 | # %% 407 | def generate_short_hash(): 408 | unique_id = uuid.uuid4() 409 | return str(unique_id).replace('-', '')[:8] 410 | 411 | 412 | def ConvertToInteractive(dn): 413 | if not dn.object().hasFn(om2.MFn.kTransform): 414 | path = om2.MDagPath.getAPathTo(om2.MFnDagNode(dn.object()).parent(0)) 415 | else: 416 | path = om2.MDagPath.getAPathTo(dn.object()) 417 | cmds.select(path, replace=True) 418 | prefix = "z" + generate_short_hash() 419 | res = cmds.xgmGroomConvert(prefix=prefix) 420 | if res is None or len(res) == 0: 421 | raise Exception("Convert to interactive failed.") 422 | sel = om2.MGlobal.getActiveSelectionList() 423 | 424 | if not res[0].startswith(prefix): 425 | fnDepNode = om2.MFnDependencyNode(sel.getDependNode(0)) 426 | if not fnDepNode.object().hasFn(om2.MFn.kTransform): 427 | fnDepNode = om2.MFnDependencyNode(om2.MFnDagNode(fnDepNode.object()).parent(0)) 428 | fnDepNode.setName('_'.join((prefix, fnDepNode.name()))) 429 | 430 | spline = om2.MFnDagNode(sel.getDagPath(0)) 431 | om2.MFnDagNode(getSaveXGenDesWindowParent()).addChild(spline.parent(0)) 432 | return spline 433 | # return curve.parent(0) 434 | 435 | 436 | # %% 437 | import ctypes 438 | from ctypes import c_void_p, c_uint64, c_ulonglong, c_float, c_int, c_char_p 439 | 440 | PtexSamplerDllFuncName = "_PtexSamplerDllFunc" 441 | 442 | 443 | class PtexSampler(object): 444 | class DllFunc: 445 | @staticmethod 446 | def getVFunc(obj, index, *args): 447 | vtble = ctypes.cast(obj, ctypes.POINTER(ctypes.c_void_p)).contents 448 | vfuncAddr = ctypes.cast(vtble.value + index * 8, ctypes.POINTER(ctypes.c_void_p)).contents.value 449 | return ctypes.CFUNCTYPE(*args)(vfuncAddr) 450 | 451 | @staticmethod 452 | def getPtexFilterEvelFunc(filter): 453 | ptexFilterEvel = PtexSampler.DllFunc.getVFunc(filter, 2, c_void_p, c_void_p, c_void_p, c_int, c_int, c_int, 454 | c_float, c_float, 455 | c_float, c_float, c_float, c_float) 456 | return ptexFilterEvel 457 | 458 | @staticmethod 459 | def getPtexTextureReleaseFunc(ptexTexture): 460 | ptexTextureRelease = PtexSampler.DllFunc.getVFunc(ptexTexture, 1, c_void_p) 461 | return ptexTextureRelease 462 | 463 | class MyVector(ctypes.Structure): 464 | _fields_ = [ 465 | ("_Myfirst", ctypes.POINTER(ctypes.c_float)), # pointer to beginning of array 466 | ("_Mylast", ctypes.POINTER(ctypes.c_float)), # pointer to current end of sequence 467 | ("_Myend", ctypes.POINTER(ctypes.c_float)) # pointer to end of sequence 468 | ] 469 | 470 | def __init__(self, size=10): 471 | array = (ctypes.c_float * size)() 472 | # 为每个指针分配内存 473 | self._Myfirst = ctypes.cast(array, ctypes.POINTER(ctypes.c_float)) 474 | # _Mylast 初始化为数组的开始(指向和 _Myfirst 相同的位置) 475 | self._Mylast = self._Myfirst # 当前结束位置指向数组的开始 476 | _myend_address = ctypes.addressof(array) + ctypes.sizeof(array) # 获取数组末尾(地址) 477 | self._Myend = ctypes.cast(_myend_address, ctypes.POINTER(ctypes.c_float)) 478 | 479 | def __getitem__(self, index): 480 | return self._Myfirst[index] 481 | 482 | def close(self): 483 | getPtexTextureRelease = PtexSampler.DllFunc.getPtexTextureReleaseFunc(self.ptexTexture) 484 | getPtexTextureRelease(self.ptexTexture) 485 | 486 | def setupFilter(self, path): 487 | DllFunc = globals()[PtexSamplerDllFuncName] 488 | err = ctypes.c_uint64() 489 | ptexTexture = DllFunc.ptex_open( 490 | path.encode(), 491 | ctypes.byref(err), 0) 492 | if ptexTexture is None: 493 | raise Exception("no ptexTexture found") 494 | self.ptexTexture = ctypes.c_void_p(ptexTexture) 495 | 496 | # 定义Options结构体 497 | class Options(ctypes.Structure): 498 | _fields_ = [ 499 | ("__structSize", ctypes.c_int), # (for internal use only) 500 | ("filter", ctypes.c_int), # Filter type. 501 | ("lerp", ctypes.c_bool), # Interpolate between mipmap levels. 502 | ("sharpness", ctypes.c_float), # Filter sharpness, 0..1 (for general bi-cubic filter only). 503 | ("noedgeblend", ctypes.c_bool) # Disable cross-face filtering. 504 | ] 505 | 506 | def __init__(self, filter_=0, lerp_=False, sharpness_=0.0, 507 | noedgeblend_=False): # Point-sampled (no filtering) 508 | self.__structSize = ctypes.sizeof(Options) # 设置结构体大小 509 | self.filter = filter_ # 设置过滤器类型 510 | self.lerp = lerp_ # 设置是否插值 511 | self.sharpness = sharpness_ # 设置过滤器锐度 512 | self.noedgeblend = noedgeblend_ # 设置是否禁用跨面过滤 513 | 514 | options = Options() 515 | filter = DllFunc.ptex_getFilter(self.ptexTexture, options) 516 | filter = ctypes.cast(filter, ctypes.c_void_p) 517 | self.ptexFilterEvalFunc = DllFunc.getPtexFilterEvelFunc(filter) 518 | self.vector = DllFunc.temp_vector 519 | self.filter = filter 520 | 521 | def __init__(self, path): 522 | self.path = path 523 | if PtexSamplerDllFuncName in globals(): 524 | self.setupFilter(path) 525 | return 526 | 527 | DllFunc = PtexSampler.DllFunc() 528 | globals()[PtexSamplerDllFuncName] = DllFunc 529 | 530 | ptex_dll = ctypes.cdll.LoadLibrary("Ptex.dll") 531 | versions = ['2_2', '2_3', '2_4', '2_5', '2_6'] 532 | version = None 533 | for _v in versions: 534 | try: 535 | Ptex_String_release = ptex_dll["??1String@v{}@Ptex@@QEAA@XZ".format(_v)] 536 | version = _v 537 | break 538 | except: 539 | pass 540 | if version is None: 541 | raise Exception("Could not find correct Ptex version") 542 | 543 | DllFunc.ptex_open = ptex_dll["?open@PtexTexture@v{}@Ptex@@SAPEAV123@PEBDAEAVString@23@_N@Z".format(version)] 544 | DllFunc.ptex_open.restype = ctypes.c_void_p 545 | DllFunc.ptex_getFilter = ptex_dll[ 546 | '?getFilter@PtexFilter@v{}@Ptex@@SAPEAV123@PEAVPtexTexture@23@AEBUOptions@123@@Z'.format(version)] 547 | DllFunc.ptex_getFilter.restype = ctypes.c_void_p 548 | DllFunc.temp_vector = DllFunc.MyVector(10) 549 | self.setupFilter(path) 550 | 551 | def sampleData(self, faceU, faceV, faceId): 552 | self.ptexFilterEvalFunc(self.filter, self.vector._Myfirst, 0, 3, faceId, faceU, faceV, 0, 0, 0, 0) 553 | return self.vector[:3] 554 | 555 | 556 | # %% 557 | class XGenProxyEveryFrame(CurvesProxy): 558 | def __init__(self, curveObj, descFnDepNode, needRootList=False, 559 | animation=False): 560 | super(XGenProxyEveryFrame, self).__init__(curveObj, None, needRootList, animation) 561 | self.descFnDepNode = descFnDepNode 562 | self.sorted_offset_map = None 563 | 564 | def write_first_frame(self): 565 | if print_debug: 566 | startTime = time.time() 567 | 568 | spline = ConvertToInteractive(self.descFnDepNode) 569 | self.fnDepNode = spline 570 | self.firstSpline = spline 571 | PrimitiveInfosList, PositionsDataList, WidthsDataList, FaceIdList, FaceUVList = getXgenData(self.fnDepNode, 572 | ('PrimitiveInfos', 573 | 'Positions', 574 | 'WIDTH_CV', 575 | 'FaceId', 576 | 'FaceUV')) 577 | # calculate sorted order first 578 | index2order = self.get_index2order(FaceIdList, FaceUVList) 579 | self.firstSplineIndex2order = index2order 580 | if print_debug: 581 | print("getXgenData: %.4f" % (time.time() - startTime)) 582 | startTime = time.time() 583 | numCurves = 0 584 | numCVs = 0 585 | for i, PrimitiveInfos in enumerate(PrimitiveInfosList): 586 | numCurves += len(PrimitiveInfos) 587 | for PrimitiveInfo in PrimitiveInfos: 588 | length = int(PrimitiveInfo[1]) 589 | if length < 2: 590 | continue 591 | numCVs += length 592 | self.numCurves = numCurves 593 | self.numCVs = numCVs 594 | orders = imath.UnsignedCharArray(numCurves) 595 | nVertices = imath.IntArray(numCurves) 596 | 597 | samp = self.firstSamp 598 | samp.setBasis(abcGeom.BasisType.kBsplineBasis) 599 | samp.setWrap(abcGeom.CurvePeriodicity.kNonPeriodic) 600 | samp.setType(abcGeom.CurveType.kCubic) 601 | 602 | degree = 3 603 | pointArray = imath.V3fArray(numCVs) 604 | widthArray = imath.FloatArray(numCVs) 605 | if self.needRootList: 606 | self.hairRootList = [None] * numCurves 607 | knots = [None] * numCurves 608 | 609 | curveIndex = 0 610 | # cvIndex = 0 611 | # cvOffsets = imath.IntArray(numCurves) 612 | curvelengths = imath.IntArray(numCurves) 613 | 614 | for j in range(len(PrimitiveInfosList)): 615 | PrimitiveInfos = PrimitiveInfosList[j] 616 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 617 | length = int(PrimitiveInfo[1]) 618 | if length < 2: 619 | continue 620 | curvelengths[index2order[curveIndex]] = length 621 | curveIndex += 1 622 | 623 | # self.sorted_offset_map = [0] + list(itertools.accumulate(curvelengths)) # py2 has not itertools.accumulate 624 | self.sorted_offset_map = [0] 625 | total = 0 626 | for length in curvelengths: 627 | total += length 628 | self.sorted_offset_map.append(total) 629 | 630 | curveIndex = 0 631 | # cvOffsets = imath.IntArray(numCurves) 632 | for j in range(len(PrimitiveInfosList)): 633 | PrimitiveInfos = PrimitiveInfosList[j] 634 | posData = PositionsDataList[j] 635 | widthData = WidthsDataList[j] 636 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 637 | offset = PrimitiveInfo[0] 638 | length = int(PrimitiveInfo[1]) 639 | if length < 2: 640 | continue 641 | startAddr = offset * 3 642 | sortedCurveIndex = index2order[curveIndex] 643 | # cvOffsets[curveIndex] = cvIndex 644 | cvIndex = self.sorted_offset_map[sortedCurveIndex] 645 | for k in range(length): 646 | pointArray[cvIndex].x = posData[startAddr] 647 | pointArray[cvIndex].y = posData[startAddr + 1] 648 | pointArray[cvIndex].z = posData[startAddr + 2] 649 | if k == 0 and self.needRootList: 650 | self.hairRootList[sortedCurveIndex] = om.MPoint(pointArray[cvIndex]) 651 | widthArray[cvIndex] = widthData[offset + k] 652 | startAddr += 3 653 | cvIndex += 1 654 | 655 | orders[sortedCurveIndex] = degree + 1 656 | nVertices[sortedCurveIndex] = length 657 | 658 | knotsInsideNum = length - degree + 1 659 | knotsList = [0] * degree + list(range(knotsInsideNum)) + [ 660 | knotsInsideNum - 1] * degree # The endpoint repeats one more than Maya 661 | # print(knotsList) 662 | knots[sortedCurveIndex] = knotsList 663 | curveIndex += 1 664 | knots = list(itertools.chain(*knots)) 665 | 666 | samp.setCurvesNumVertices(nVertices) 667 | samp.setPositions(pointArray) 668 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 669 | samp.setOrders(orders) 670 | 671 | widths = abcGeom.OFloatGeomParamSample(widthArray, abcGeom.GeometryScope.kVertexScope) 672 | samp.setWidths(widths) 673 | self.schema.set(samp) 674 | # if self.animation: 675 | # index2order = self.get_index2order(FaceIdList, FaceUVList) 676 | # self.order_offset_map = imath.IntArray(numCurves) 677 | # for i, offset in zip(index2order, cvOffsets): 678 | # self.order_offset_map[i] = offset 679 | if print_debug: 680 | # print(self.order_offset_map) 681 | print("write_first_frame: %.4f" % (time.time() - startTime)) 682 | 683 | @staticmethod 684 | def get_index2order(FaceIdList, FaceUVList): 685 | order_list = [] 686 | for j in range(len(FaceIdList)): 687 | FaceUVData = FaceUVList[j] 688 | FaceIdData = FaceIdList[j] 689 | for i, faceId in enumerate(FaceIdData): 690 | u = FaceUVData[i * 2] 691 | v = FaceUVData[i * 2 + 1] 692 | order_list.append((faceId, u, v)) 693 | sorted_list = sorted((key, i) for i, key in enumerate(order_list)) 694 | index_list = imath.IntArray(len(order_list)) 695 | for order_index, item in enumerate(sorted_list): 696 | my_index = item[1] 697 | index_list[my_index] = order_index 698 | # if print_debug: 699 | # print(sorted_list) 700 | return index_list 701 | 702 | def write_frame(self): 703 | if print_debug: 704 | startTime = time.time() 705 | spline = ConvertToInteractive(self.descFnDepNode) 706 | self.fnDepNode = spline 707 | PrimitiveInfosList, PositionsDataList, FaceIdList, FaceUVList = getXgenData(self.fnDepNode, ('PrimitiveInfos', 708 | 'Positions', 709 | 'FaceId', 710 | 'FaceUV')) 711 | 712 | numCVs = self.numCVs 713 | 714 | samp = abcGeom.OCurvesSchemaSample() 715 | samp.setBasis(self.firstSamp.getBasis()) 716 | samp.setWrap(self.firstSamp.getWrap()) 717 | samp.setType(self.firstSamp.getType()) 718 | 719 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 720 | samp.setKnots(self.firstSamp.getKnots()) 721 | samp.setOrders(self.firstSamp.getOrders()) 722 | samp.setWidths(self.firstSamp.getWidths()) 723 | 724 | if print_debug: 725 | s = time.time() 726 | index2order = self.get_index2order(FaceIdList, FaceUVList) 727 | 728 | pointArray = imath.V3fArray(numCVs) 729 | 730 | curveIndex = 0 731 | for j in range(len(PrimitiveInfosList)): 732 | PrimitiveInfos = PrimitiveInfosList[j] 733 | posData = PositionsDataList[j] 734 | for PrimitiveInfo in PrimitiveInfos: 735 | offset = PrimitiveInfo[0] 736 | length = int(PrimitiveInfo[1]) 737 | if length < 2: 738 | continue 739 | startAddr = offset * 3 740 | cvIndex = self.sorted_offset_map[index2order[curveIndex]] 741 | for k in range(length): 742 | pointArray[cvIndex].x = posData[startAddr] 743 | pointArray[cvIndex].y = posData[startAddr + 1] 744 | pointArray[cvIndex].z = posData[startAddr + 2] 745 | startAddr += 3 746 | cvIndex += 1 747 | 748 | curveIndex += 1 749 | if print_debug: 750 | print("loop: %.4f" % (time.time() - s)) 751 | samp.setPositions(pointArray) 752 | 753 | self.schema.set(samp) 754 | if print_debug: 755 | print("write_frame: %.4f" % (time.time() - startTime)) 756 | 757 | 758 | # %% 759 | 760 | GroomGuideIdStartIndexName = '_GroomGuideIdStartIndexName' 761 | 762 | 763 | def getGroomGuideIdStartIndex(): 764 | return globals()[GroomGuideIdStartIndexName] 765 | 766 | 767 | def setGroomGuideIdStartIndex(value=0): 768 | globals()[GroomGuideIdStartIndexName] = value 769 | 770 | 771 | # %% 772 | class GuideProxy(CurvesProxy): 773 | def __init__(self, curveObj, fnDepNode, needRootList=False, animation=False): 774 | if not fnDepNode.object().hasFn(om2.MFn.kTransform): 775 | fnDepNode = om2.MFnDependencyNode(om2.MFnDagNode(fnDepNode.object()).parent(0)) 776 | super(GuideProxy, self).__init__(curveObj, fnDepNode, needRootList, animation) 777 | itDag = om2.MItDag() 778 | itDag.reset(self.fnDepNode.object(), om2.MItDag.kBreadthFirst, om2.MFn.kInvalid) 779 | guides = [] 780 | while not itDag.isDone(): 781 | dn = om2.MFnDependencyNode(itDag.currentItem()) 782 | if dn.typeName == 'xgmSplineGuide': 783 | guides.append(itDag.getPath()) 784 | itDag.next() 785 | self.guides = guides 786 | self.xgenProxy = None 787 | self.ptexPath = None 788 | self.writePtexGuideId = False 789 | 790 | def set_xgen_proxy_and_ptex(self, xgenSpline, ptexPath): 791 | self.xgenProxy = xgenSpline 792 | self.ptexPath = ptexPath 793 | 794 | def write_guide_id_from_ptex(self): 795 | if not self.writePtexGuideId: 796 | return 797 | ptexPath = self.ptexPath 798 | xgenSpline = self.xgenProxy 799 | if ptexPath is None or ptexPath == "": 800 | return 801 | if self.xgenProxy == None: 802 | return 803 | ptexSampler = PtexSampler(ptexPath) 804 | self.regionPtex = ptexPath 805 | 806 | guide_map = dict() 807 | 808 | def color2Int(color): 809 | a = int(color[0] * 255) & 0xff 810 | b = int(color[1] * 255) & 0xff 811 | c = int(color[2] * 255) & 0xff 812 | return (a << 16) | (b << 8) | c 813 | 814 | for i, guide in enumerate(self.guides): 815 | dn = om2.MFnDependencyNode(guide.node()) 816 | u = dn.findPlug('uLoc', False).asFloat() 817 | v = dn.findPlug('vLoc', False).asFloat() 818 | faceId = dn.findPlug('faceId', False).asInt() 819 | color = ptexSampler.sampleData(u, v, faceId) 820 | hash = color2Int(color) 821 | if hash in guide_map: 822 | old_guide = om2.MFnDependencyNode(guide_map[hash][0].node()) 823 | print( 824 | "guide {} and {} are in the same area on texture, only use {}.".format(dn.name(), old_guide.name(),old_guide.name())) 825 | continue 826 | guide_map[hash] = (guide, i) 827 | 828 | FaceUVList, FaceIdList = getXgenData(xgenSpline.firstSpline, ('FaceUV', 'FaceId')) 829 | 830 | guideIdStartIndex = getGroomGuideIdStartIndex() 831 | guideIdNextStartIndex = guideIdStartIndex + len(self.guides) 832 | groom_id_data = list2ImathArray(list(range(guideIdStartIndex, guideIdNextStartIndex)), imath.IntArray) 833 | self.write_param("groom_id", AbcType.int32, groom_id_data) 834 | 835 | spline_num = len(xgenSpline.hairRootList) 836 | weight_data = list2ImathArray([1.0] * spline_num, imath.FloatArray) 837 | xgenSpline.write_param("groom_guide_weights", AbcType.float, weight_data) 838 | 839 | guide_id_data = imath.IntArray(spline_num) 840 | first_guide_name = om2.MFnDependencyNode(self.guides[0].node()).name() 841 | spline_index = 0 842 | for j in range(len(FaceIdList)): 843 | FaceUVData = FaceUVList[j] 844 | FaceIdData = FaceIdList[j] 845 | # print(len(FaceIdData),len(FaceUVData)) 846 | for i, faceId in enumerate(FaceIdData): 847 | u = FaceUVData[i * 2] 848 | v = FaceUVData[i * 2 + 1] 849 | color = ptexSampler.sampleData(u, v, faceId) 850 | hash = color2Int(color) 851 | guide_id = guideIdStartIndex 852 | if hash not in guide_map: 853 | print( 854 | "The spline index ({} ,{}) does not have a valid guide attached to {}.".format( 855 | j, i, first_guide_name)) 856 | else: 857 | guide_id = guide_map[hash][1] 858 | 859 | if spline_index >= spline_num: 860 | raise Exception("spline_index >= spline_num") 861 | sorted_spine_index = xgenSpline.firstSplineIndex2order[spline_index] 862 | guide_id_data[sorted_spine_index] = guide_id 863 | # guide_map[hash][2].append(spline_index) 864 | spline_index += 1 865 | # print(guide_map) 866 | ptexSampler.close() 867 | xgenSpline.write_param("groom_closest_guides", AbcType.int32, guide_id_data) 868 | 869 | setGroomGuideIdStartIndex(guideIdNextStartIndex) 870 | 871 | def write_first_frame(self): 872 | numCurves = len(self.guides) 873 | orders = imath.IntArray(numCurves) 874 | nVertices = imath.IntArray(numCurves) 875 | pointslist = [] 876 | knots = [] 877 | if self.needRootList: 878 | self.hairRootList = [] 879 | 880 | samp = self.firstSamp 881 | samp.setBasis(abcGeom.BasisType.kBsplineBasis) 882 | samp.setWrap(abcGeom.CurvePeriodicity.kNonPeriodic) 883 | samp.setType(abcGeom.CurveType.kLinear) 884 | degree = 1 885 | 886 | for i in range(numCurves): 887 | data = cmds.xgmGuideGeom(guide=self.guides[i], numVertices=True) 888 | numCVs = int(data[0]) 889 | data = cmds.xgmGuideGeom(guide=self.guides[i], controlPoints=True) 890 | pointslist += data 891 | orders[i] = degree + 1 892 | nVertices[i] = numCVs 893 | if self.needRootList: 894 | self.hairRootList.append(om2.MPoint(data[:3])) 895 | 896 | knotsInsideNum = numCVs - degree + 1 897 | knotsList = [0] * degree + list(range(knotsInsideNum)) + [ 898 | knotsInsideNum - 1] * degree # The endpoint repeats one more than Maya 899 | # print(knotsList) 900 | knots += knotsList 901 | samp.setCurvesNumVertices(nVertices) 902 | samp.setPositions(floatList2V3fArray(pointslist)) 903 | samp.setOrders(list2ImathArray(orders, imath.UnsignedCharArray)) 904 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 905 | self.schema.set(samp) 906 | 907 | def write_frame(self): 908 | numCurves = len(self.guides) 909 | if numCurves == 0: 910 | return 911 | 912 | samp = abcGeom.OCurvesSchemaSample() 913 | samp.setBasis(self.firstSamp.getBasis()) 914 | samp.setWrap(self.firstSamp.getWrap()) 915 | samp.setType(self.firstSamp.getType()) 916 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 917 | samp.setOrders(self.firstSamp.getOrders()) 918 | samp.setKnots(self.firstSamp.getKnots()) 919 | 920 | pointslist = [] 921 | for i in range(numCurves): 922 | data = cmds.xgmGuideGeom(guide=self.guides[i], controlPoints=True) 923 | pointslist += data 924 | 925 | samp.setPositions(floatList2V3fArray(pointslist)) 926 | self.schema.set(samp) 927 | 928 | 929 | # %% 930 | 931 | class SaveXGenDesWindow(QtWidgets.QDialog): 932 | class MultiSelectCheckBox(QtWidgets.QCheckBox): 933 | def __init__(self, column_name, parent=None): 934 | super(SaveXGenDesWindow.MultiSelectCheckBox,self).__init__(parent) 935 | self.column_name = column_name 936 | self.clicked.connect(lambda: self.on_clicked(self.isChecked())) 937 | 938 | def on_clicked(self, checked): 939 | window = self.find_window() 940 | if not window or not hasattr(window, 'table'): 941 | return 942 | table = window.table 943 | contents = window.contentList 944 | selected_rows = self.get_rows_to_changing(table) 945 | for row in selected_rows: 946 | if 0 <= row < len(contents): 947 | content = contents[row] 948 | checkbox = getattr(content, self.column_name) 949 | if checkbox is not None: 950 | checkbox.blockSignals(True) 951 | checkbox.setChecked(checked) 952 | checkbox.blockSignals(False) 953 | 954 | def find_window(self): 955 | parent = self.parent() 956 | while parent: 957 | if (isinstance(parent, SaveXGenDesWindow) or 958 | parent.__class__.__name__ == 'SaveXGenDesWindow'): 959 | return parent 960 | parent = parent.parent() 961 | return None 962 | 963 | def get_rows_to_changing(self, table): 964 | pos = self.mapTo(table.viewport(), QtCore.QPoint(0, 0)) 965 | _index = table.indexAt(pos).row() 966 | selected_rows = [index.row() for index in table.selectionModel().selectedRows()] 967 | return selected_rows if _index in selected_rows else [_index] 968 | 969 | class Content: 970 | def __init__(self, fnDepNode, showName, groupName, useGuide, bakeUV, animation, export): 971 | self.showName = showName 972 | self.fnDepNode = fnDepNode 973 | self.groupName = QtWidgets.QLineEdit() 974 | self.groupName.setText(groupName) 975 | self.useGuide = SaveXGenDesWindow.MultiSelectCheckBox("useGuide") 976 | self.useGuide.setChecked(useGuide) 977 | self.bakeUV = SaveXGenDesWindow.MultiSelectCheckBox("bakeUV") 978 | self.bakeUV.setChecked(bakeUV) 979 | self.animation = SaveXGenDesWindow.MultiSelectCheckBox("animation") 980 | self.animation.setChecked(animation) 981 | self.export = SaveXGenDesWindow.MultiSelectCheckBox("export") 982 | self.export.setChecked(export) 983 | self.splineAnimation = False 984 | self.writePtexGuideId = False 985 | self.regionPtex = "" 986 | 987 | def __init__(self, parent=mayaWindow()): 988 | super(SaveXGenDesWindow, self).__init__(parent) 989 | self.contentList = [] 990 | self.save_path = '.' 991 | self.bakeMesh = None 992 | self.setWindowTitle("Export XGen description to UE Groom v{}".format(_XGenExporterVersion)) 993 | self.setGeometry(400, 400, 1130, 550) 994 | self.buildUI() 995 | 996 | def showAbout(self): 997 | QtWidgets.QMessageBox.about(self, "Export XGen to UE Groom", 998 | "A small tool to export XGen to UE Groom, by PDE26jjk. Link: https://github.com/PDE26jjk/XGenUEGroomExporter") 999 | 1000 | def createFrame(self, labelText): 1001 | try: 1002 | frame = om1ui.MQtUtil.findControl( 1003 | cmds.frameLayout(label=labelText, collapsable=True, collapse=True, manage=True)) 1004 | frame = shiboken.wrapInstance(int(frame), QtWidgets.QWidget) 1005 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 1006 | frameLayout = frame.children()[2].children()[0] 1007 | except: 1008 | frame = QtWidgets.QFrame(self) 1009 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 1010 | frameLayout = QtWidgets.QVBoxLayout(frame) 1011 | frame.children().append(frameLayout) 1012 | return frame, frameLayout 1013 | 1014 | def buildUI(self): 1015 | main_layout = QtWidgets.QVBoxLayout() 1016 | 1017 | menu_bar = QtWidgets.QMenuBar(self) 1018 | menu_bar.addMenu("Help").addAction("About", self.showAbout) 1019 | main_layout.setMenuBar(menu_bar) 1020 | 1021 | label1 = QtWidgets.QLabel("Select XGen Description") 1022 | label1.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 1023 | hBox = QtWidgets.QHBoxLayout() 1024 | hBox.setContentsMargins(10, 4, 10, 4) 1025 | hBox.addWidget(label1) 1026 | 1027 | self.fillWithSelectList_button = QtWidgets.QPushButton("Refresh selected") 1028 | self.fillWithSelectList_button.clicked.connect(self.fillWithSelectList) 1029 | self.fillWithSelectList_button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 1030 | # main_layout.addWidget(self.fillWithSelectList_button) 1031 | hBox.addStretch(1) 1032 | hBox.addWidget(self.fillWithSelectList_button) 1033 | main_layout.addLayout(hBox) 1034 | 1035 | self.table = QtWidgets.QTableWidget(self) 1036 | self.table.setColumnCount(7) 1037 | self.table.setHorizontalHeaderLabels(["", "Name", "Group name", "Use guide", "Bake UV", "Animation", ""]) 1038 | self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) 1039 | self.table.setColumnWidth(0, 40) 1040 | self.table.setColumnWidth(3, 140) 1041 | self.table.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.Fixed) 1042 | self.table.setColumnWidth(4, 140) 1043 | self.table.horizontalHeader().setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) 1044 | self.table.setColumnWidth(5, 140) 1045 | self.table.horizontalHeader().setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) 1046 | self.table.setColumnWidth(6, 140) 1047 | self.table.horizontalHeader().setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) 1048 | self.table.horizontalHeader().setStretchLastSection(True) 1049 | self.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) # Multi Selection 1050 | self.table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) 1051 | 1052 | self.table.setStyleSheet(""" 1053 | QTableView::item 1054 | { 1055 | border: 0px; 1056 | padding: 5px; 1057 | background-color: rgb(68, 68, 68); 1058 | } 1059 | QTableView::item:selected { 1060 | background-color: rgb(81, 133, 166); 1061 | } 1062 | QTableView::item QCheckBox { 1063 | padding-left:60px; 1064 | } 1065 | """) 1066 | 1067 | self.table.clearContents() 1068 | self.table.setRowCount(0) 1069 | 1070 | self.splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) 1071 | 1072 | # self.table.cellClicked.connect(self.update_detail) 1073 | self.table.selectionModel().selectionChanged.connect(self.update_detail) 1074 | self.splitter.addWidget(self.table) 1075 | 1076 | # Detail view on the right 1077 | self.detail_widget = None 1078 | self.clear_detail() 1079 | # self.splitter.setSizes([1,0]) 1080 | 1081 | self.Bakeframe, frameLayout = self.createFrame(labelText="Bake UV") 1082 | 1083 | self.MeshName = QtWidgets.QLabel("Mesh : ---") 1084 | hBox = QtWidgets.QHBoxLayout() 1085 | hBox.setContentsMargins(10, 10, 10, 10) 1086 | hBox.addWidget(self.MeshName) 1087 | hBox2 = QtWidgets.QHBoxLayout() 1088 | label = QtWidgets.QLabel("UV Set : ") 1089 | hBox2.addWidget(label) 1090 | self.combo = QtWidgets.QComboBox() 1091 | self.combo.addItem(" --- ") 1092 | 1093 | self.uvSetStr = QtWidgets.QLabel("Selected: None") 1094 | 1095 | self.combo.currentIndexChanged.connect(self.update_uvset_label) 1096 | hBox2.addWidget(self.combo) 1097 | hBox.addStretch(2) 1098 | hBox.addLayout(hBox2) 1099 | hBox.addStretch(1) 1100 | 1101 | frameLayout.addLayout(hBox) 1102 | 1103 | self.button3 = QtWidgets.QPushButton("Pick other mesh", self) 1104 | self.button3.clicked.connect(self.pick_mesh) 1105 | self.button3.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 1106 | frameLayout.addWidget(self.button3) 1107 | 1108 | self.separator = QtWidgets.QFrame(self) 1109 | self.separator.setFrameShape(QtWidgets.QFrame.HLine) 1110 | self.separator.setFrameShadow(QtWidgets.QFrame.Sunken) 1111 | 1112 | self.AnimationFrame, frameLayout = self.createFrame(labelText="Animation") 1113 | 1114 | validator = QtGui.QIntValidator() 1115 | validator.setRange(0, 99999) 1116 | self.startFrame = QtWidgets.QLineEdit() 1117 | self.startFrame.setMaximumWidth(60) 1118 | self.startFrame.setValidator(validator) 1119 | self.startFrame.setText(str(0)) 1120 | self.endFrame = QtWidgets.QLineEdit() 1121 | self.endFrame.setMaximumWidth(60) 1122 | self.endFrame.setValidator(validator) 1123 | self.endFrame.setText(str(0)) 1124 | self.preroll = QtWidgets.QCheckBox("Preroll") 1125 | 1126 | frameLayout.setContentsMargins(10, 10, 10, 10) 1127 | hBox = QtWidgets.QHBoxLayout() 1128 | hBox.addWidget(QtWidgets.QLabel("Frame Range : ")) 1129 | hBox.addWidget(self.startFrame) 1130 | hBox.addWidget(QtWidgets.QLabel(" ~ ")) 1131 | hBox.addWidget(self.endFrame) 1132 | hBox.addStretch(1) 1133 | hBox2 = QtWidgets.QHBoxLayout() 1134 | hBox2.addWidget(self.preroll) 1135 | hBox2.addStretch(1) 1136 | 1137 | frameLayout.addLayout(hBox) 1138 | frameLayout.addLayout(hBox2) 1139 | 1140 | self.SettingFrame, frameLayout = self.createFrame(labelText="Setting") 1141 | 1142 | frameLayout.setContentsMargins(10, 10, 10, 10) 1143 | hBox = QtWidgets.QHBoxLayout() 1144 | self.createGroupId_cb = QtWidgets.QCheckBox("Create group id") 1145 | self.createGroupId_cb.setChecked(True) 1146 | hBox.addWidget(self.createGroupId_cb) 1147 | 1148 | self.createCardId_cb = QtWidgets.QCheckBox("Create card id same as group name") 1149 | self.createCardId_cb.setChecked(False) 1150 | hBox.addWidget(self.createCardId_cb) 1151 | 1152 | frameLayout.addLayout(hBox) 1153 | 1154 | self.save_button = QtWidgets.QPushButton("Save Alembic File", self) 1155 | self.save_button.clicked.connect(self.save_abc) 1156 | self.clear_temp_button = QtWidgets.QPushButton("Clear Temp Data", self) 1157 | self.clear_temp_button.clicked.connect(self.clear_temp) 1158 | self.cancel_button = QtWidgets.QPushButton("Close", self) 1159 | self.cancel_button.clicked.connect(self.close) 1160 | 1161 | button_layout = QtWidgets.QHBoxLayout() 1162 | button_layout.addWidget(self.save_button) 1163 | button_layout.addWidget(self.clear_temp_button) 1164 | button_layout.addWidget(self.cancel_button) 1165 | 1166 | main_layout.addWidget(self.splitter) 1167 | main_layout.addWidget(self.Bakeframe) 1168 | main_layout.addWidget(self.AnimationFrame) 1169 | main_layout.addWidget(self.SettingFrame) 1170 | main_layout.addWidget(self.separator) 1171 | main_layout.addLayout(button_layout) 1172 | 1173 | self.setLayout(main_layout) 1174 | 1175 | def clear_detail(self): 1176 | old_sizes = None 1177 | if self.detail_widget is not None: 1178 | old_sizes = self.splitter.sizes() 1179 | self.detail_widget.setParent(None) 1180 | self.detail_label = QtWidgets.QLabel("Select an item to view details") 1181 | self.detail_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 1182 | self.detail_widget = QtWidgets.QWidget() 1183 | self.detail_layout = QtWidgets.QVBoxLayout() 1184 | self.detail_layout.addWidget(self.detail_label) 1185 | self.detail_widget.setLayout(self.detail_layout) 1186 | self.splitter.addWidget(self.detail_widget) 1187 | # self.detail_widget.setMaximumWidth(1000) 1188 | if old_sizes is None: 1189 | self.splitter.setStretchFactor(0, 4) 1190 | self.splitter.setStretchFactor(1, 1) 1191 | else: 1192 | self.splitter.setSizes(old_sizes) 1193 | 1194 | def create_detail_checkBox(self, prop): 1195 | selected_rows = [index.row() for index in self.table.selectionModel().selectedRows()] 1196 | if len(selected_rows) == 0: 1197 | return None 1198 | checkBox = QtWidgets.QCheckBox(self) 1199 | isTrue = getattr(self.contentList[selected_rows[0]],prop) 1200 | checkBox.setChecked(isTrue) 1201 | for row in selected_rows: 1202 | content = self.contentList[row] 1203 | if getattr(content,prop) != isTrue: 1204 | checkBox.setCheckState(QtCore.Qt.CheckState.PartiallyChecked) 1205 | break 1206 | 1207 | def onStateChange(state): 1208 | for row in selected_rows: 1209 | content = self.contentList[row] 1210 | setattr(content, prop, state == 2) 1211 | 1212 | checkBox.stateChanged.connect(onStateChange) 1213 | return checkBox 1214 | 1215 | def update_detail(self, indices): 1216 | self.clear_detail() 1217 | selected_rows = [index.row() for index in self.table.selectionModel().selectedRows()] 1218 | if len(selected_rows) == 0: 1219 | return 1220 | content = self.contentList[selected_rows[0]] 1221 | is_multi_selected = len(self.table.selectionModel().selectedRows()) > 1 1222 | vBox = QtWidgets.QVBoxLayout() 1223 | self.detail_layout.addLayout(vBox) 1224 | vBox2 = QtWidgets.QVBoxLayout() 1225 | self.detail_layout.addLayout(vBox2) 1226 | vBox2.addStretch(1) 1227 | 1228 | self.detail_label.setText(content.showName if not is_multi_selected else "--") 1229 | self.detail_label.setStyleSheet('font-weight:bold;margin-bottom:20px') 1230 | self.detail_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignCenter) # 设置对齐方式 1231 | 1232 | write_spline_animation = self.create_detail_checkBox('splineAnimation') 1233 | write_spline_animation.setText("write spline animation") 1234 | write_spline_animation.setToolTip( 1235 | "If writes animation, also write spline animation, not just the animation of guides.") 1236 | 1237 | vBox.addWidget(write_spline_animation) 1238 | 1239 | write_guide_id_cb = self.create_detail_checkBox('writePtexGuideId') 1240 | write_guide_id_cb.setText("write guide id from ptex") 1241 | write_guide_id_cb.setToolTip( 1242 | "Experimental feature, only supports versions up to UE5.3, writes properties such as groom_closest_guides.") 1243 | 1244 | # vBox.setContentsMargins(0,0,10,10) 1245 | vBox.addWidget(write_guide_id_cb) 1246 | 1247 | if is_multi_selected: 1248 | group_name_multi_edit = QtWidgets.QLineEdit() 1249 | 1250 | names_are_same = True 1251 | group_name = content.groupName.text() 1252 | for selected_row in selected_rows[1:]: 1253 | if self.contentList[selected_row].groupName.text() != group_name: 1254 | names_are_same = False 1255 | break 1256 | if names_are_same: 1257 | group_name_multi_edit.setText(group_name) 1258 | else: 1259 | group_name_multi_edit.setPlaceholderText(" -- ") 1260 | 1261 | def change_group_name(text): 1262 | for selected_row in selected_rows: 1263 | self.contentList[selected_row].groupName.setText(text) 1264 | 1265 | group_name_multi_edit.textEdited.connect(change_group_name) 1266 | hBox = QtWidgets.QHBoxLayout() 1267 | group_name_label = QtWidgets.QLabel("Group name: ") 1268 | hBox.addWidget(group_name_label) 1269 | hBox.addWidget(group_name_multi_edit) 1270 | vBox.addLayout(hBox) 1271 | 1272 | 1273 | if not is_multi_selected: 1274 | label = QtWidgets.QLabel('Select .ptx file:') 1275 | # label.setStyleSheet('font-size:16px') 1276 | vBox.addWidget(label) 1277 | def setPath(text): 1278 | content.regionPtex = text 1279 | 1280 | RegionPtex = FileSelectorWidget(setPath) 1281 | RegionPtex.set_file_path(content.regionPtex) 1282 | vBox.addWidget(RegionPtex) 1283 | 1284 | self.splitter.addWidget(self.detail_widget) 1285 | 1286 | def pick_mesh(self): 1287 | selectionList = om2.MGlobal.getActiveSelectionList() 1288 | if selectionList.length() > 0: 1289 | dag_path = selectionList.getDagPath(0) 1290 | fnDepNode = om2.MFnDependencyNode(dag_path.node()) 1291 | itDag = om2.MItDag() 1292 | # find mesh 1293 | itDag.reset(fnDepNode.object(), om2.MItDag.kDepthFirst, om2.MFn.kMesh) 1294 | while not itDag.isDone(): 1295 | meshPath = om2.MDagPath.getAPathTo(itDag.currentItem()) 1296 | mesh = om2.MFnMesh(meshPath) 1297 | self.setBakeMesh(mesh) 1298 | break 1299 | 1300 | def update_uvset_label(self): 1301 | selected_option = self.combo.currentText() 1302 | self.uvSetStr.setText(selected_option) 1303 | 1304 | def clear_temp(self): 1305 | deleteSaveXGenDesWindowParent() 1306 | 1307 | def save_abc(self): 1308 | if len(self.contentList) == 0: 1309 | print("No content") 1310 | return 1311 | file_path = cmds.fileDialog2( 1312 | caption="Save Alembic File", 1313 | fileMode=0, 1314 | okCaption="save", 1315 | startingDirectory=self.save_path, 1316 | ff='Alembic Files (*.abc);;All Files (*)' 1317 | ) 1318 | if file_path: 1319 | self.save_path = file_path[0] 1320 | else: 1321 | return 1322 | selectionList = om2.MGlobal.getActiveSelectionList() 1323 | startTime = time.time() 1324 | oldCurTime = omAnim.MAnimControl.currentTime() 1325 | archive = abc.OArchive(str(file_path[0])) 1326 | 1327 | anyAnimation = False 1328 | for item in self.contentList: 1329 | if item.export.isChecked(): 1330 | hasAnimation = item.animation.isChecked() 1331 | if hasAnimation: 1332 | anyAnimation = True 1333 | 1334 | if anyAnimation: 1335 | frameRange = [int(self.startFrame.text()), int(self.endFrame.text())] 1336 | if (frameRange[0] > frameRange[1] 1337 | or frameRange[0] < omAnim.MAnimControl.minTime().value 1338 | or frameRange[1] > omAnim.MAnimControl.maxTime().value): 1339 | raise ValueError("Frame out of range.") 1340 | # frameRange[0] = int(max(frameRange[0], omAnim.MAnimControl.minTime().value)) 1341 | # frameRange[1] = int(min(frameRange[1], omAnim.MAnimControl.maxTime().value)) 1342 | 1343 | sec = om2.MTime(1, om2.MTime.kSeconds) 1344 | spf = 1.0 / sec.asUnits(om2.MTime.uiUnit()) 1345 | timeSampling = abcA.TimeSampling(spf, spf * frameRange[0]) 1346 | 1347 | timeIndex = archive.addTimeSampling(timeSampling) 1348 | proxyList = [] # All Alembic content should be destroyed at the end of the method, otherwise it will not be written to the file 1349 | setGroomGuideIdStartIndex(0) 1350 | for item in self.contentList: 1351 | if item.export.isChecked(): 1352 | fnDepNode = item.fnDepNode 1353 | needBakeUV = item.bakeUV.isChecked() 1354 | hasAnimation = item.animation.isChecked() 1355 | useGuide = item.useGuide.isChecked() 1356 | if hasAnimation: 1357 | curveObj = abcGeom.OCurves(archive.getTop(), str(fnDepNode.name()), timeIndex) 1358 | else: 1359 | curveObj = abcGeom.OCurves(archive.getTop(), str(fnDepNode.name())) 1360 | xgenProxy = XGenProxyEveryFrame(curveObj, item.fnDepNode, needBakeUV | useGuide, item.splineAnimation) 1361 | xgenProxy.needBakeUV = needBakeUV 1362 | xgenProxy.write_group_name(item.groupName.text()) 1363 | if useGuide: 1364 | # guides = GuidesToCurves(item.fnDepNode) 1365 | guideName = fnDepNode.name() + "_guide" 1366 | if hasAnimation: 1367 | curveObj = abcGeom.OCurves(archive.getTop(), str(guideName), timeIndex) 1368 | else: 1369 | curveObj = abcGeom.OCurves(archive.getTop(), str(guideName)) 1370 | guideProxy = GuideProxy(curveObj, fnDepNode, False, hasAnimation) 1371 | guideProxy.write_group_name(item.groupName.text()) 1372 | guideProxy.write_is_guide(True) 1373 | proxyList.append(guideProxy) 1374 | guideProxy.set_xgen_proxy_and_ptex(xgenProxy, item.regionPtex) 1375 | guideProxy.writePtexGuideId = item.writePtexGuideId 1376 | proxyList.append(xgenProxy) # after guides, for baking 1377 | # return 1378 | if len(proxyList) == 0: 1379 | print("No content") 1380 | om2.MGlobal.setActiveSelectionList(selectionList) 1381 | return 1382 | 1383 | if self.createGroupId_cb.isChecked(): 1384 | groupIds = dict() 1385 | currentId = 0 1386 | for proxy in proxyList: 1387 | if proxy.groupName not in groupIds: 1388 | groupIds[proxy.groupName] = currentId 1389 | currentId += 1 1390 | proxy.write_group_id(groupIds[proxy.groupName]) 1391 | 1392 | if anyAnimation: 1393 | if self.preroll.isChecked(): 1394 | for frame in range(int(omAnim.MAnimControl.minTime().value), frameRange[0]): 1395 | om1.MGlobal.viewFrame(frame) 1396 | for frame in range(frameRange[0], frameRange[1] + 1): 1397 | om1.MGlobal.viewFrame(frame) 1398 | for item in proxyList: 1399 | if frame == frameRange[0]: 1400 | item.write_first_frame() 1401 | elif item.animation: 1402 | item.write_frame() 1403 | omAnim.MAnimControl.setCurrentTime(oldCurTime) 1404 | else: 1405 | for item in proxyList: 1406 | item.write_first_frame() 1407 | for item in proxyList: 1408 | item.bake_uv(self.bakeMesh, self.uvSetStr.text()) 1409 | if isinstance(item, GuideProxy): 1410 | item.write_guide_id_from_ptex() 1411 | print("Data has been saved in %s, it took %.2f seconds." % (file_path[0], time.time() - startTime)) 1412 | om2.MGlobal.setActiveSelectionList(selectionList) 1413 | return file_path[0] 1414 | 1415 | def fillWithSelectList(self): 1416 | self.clear_detail() 1417 | self.contentList = [] 1418 | selectionList = om2.MGlobal.getActiveSelectionList() 1419 | contentList = [] 1420 | for i in range(selectionList.length()): 1421 | dag_path = selectionList.getDagPath(i) 1422 | fnDepNode = om2.MFnDependencyNode(dag_path.node()) 1423 | if fnDepNode.typeName == 'xgmPalette': 1424 | continue 1425 | itDag = om2.MItDag() 1426 | # find xgen description 1427 | itDag.reset(fnDepNode.object(), om2.MItDag.kDepthFirst, om2.MFn.kNamedObject) 1428 | xgDes = None 1429 | while not itDag.isDone(): 1430 | dn = om2.MFnDependencyNode(itDag.currentItem()) 1431 | if dn.typeName == 'xgmDescription': 1432 | xgDes = dn 1433 | break 1434 | itDag.next() 1435 | if xgDes is not None: 1436 | content = SaveXGenDesWindow.Content(xgDes, fnDepNode.name(), fnDepNode.name(), True, 1437 | False, False, True) 1438 | content.regionPtex = getClumpingPtexPath(fnDepNode) 1439 | contentList.append(content) 1440 | boundMesh = self.findBoundMesh(xgDes) 1441 | if boundMesh is not None: 1442 | self.setBakeMesh(boundMesh) 1443 | 1444 | self.table.setRowCount(len(contentList)) 1445 | for row in range(len(contentList)): 1446 | self.table.setCellWidget(row, 0, contentList[row].export) 1447 | contentList[row].export.setStyleSheet("padding-left:8px") 1448 | item = QtWidgets.QTableWidgetItem(contentList[row].showName) 1449 | item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable) 1450 | self.table.setItem(row, 1, item) 1451 | 1452 | self.table.setCellWidget(row, 2, contentList[row].groupName) 1453 | self.table.setCellWidget(row, 3, contentList[row].useGuide) 1454 | self.table.setCellWidget(row, 4, contentList[row].bakeUV) 1455 | self.table.setCellWidget(row, 5, contentList[row].animation) 1456 | 1457 | self.contentList = contentList 1458 | 1459 | def findBoundMesh(self, xgDes): 1460 | itDg = om2.MItDag() 1461 | itDg.reset(om2.MFnDagNode(xgDes.object()).parent(0), om2.MItDag.kDepthFirst, om2.MFn.kPluginShape) 1462 | boundMesh = None 1463 | while not itDg.isDone(): 1464 | dn = om2.MFnDependencyNode(itDg.currentItem()) 1465 | if dn.typeName == 'xgmSubdPatch': 1466 | boundMeshPlug = dn.findPlug('geometry', False) 1467 | boundMesh = om2.MFnMesh( 1468 | om2.MDagPath.getAPathTo(boundMeshPlug.source().node())) 1469 | break 1470 | itDg.next() 1471 | return boundMesh 1472 | 1473 | def setBakeMesh(self, mesh): 1474 | if mesh is not None: 1475 | self.bakeMesh = mesh 1476 | self.MeshName.setText("Mesh: {}".format(mesh.name())) 1477 | self.combo.clear() 1478 | self.combo.addItems(mesh.getUVSetNames()) 1479 | 1480 | 1481 | SaveXGenDesWindowInstanceName = '_SaveXGenDesWindowInstance' 1482 | if SaveXGenDesWindowInstanceName not in globals(): 1483 | globals()[SaveXGenDesWindowInstanceName] = SaveXGenDesWindow() 1484 | globals()[SaveXGenDesWindowInstanceName].show() 1485 | # SaveXGenDesWindow().show() 1486 | -------------------------------------------------------------------------------- /XGenDescriptionUEGroomExporter.py: -------------------------------------------------------------------------------- 1 | # %% 2 | import alembic.Abc as abc 3 | import alembic.AbcGeom as abcGeom 4 | import alembic.AbcCoreAbstract as abcA 5 | import maya.OpenMaya as om1 6 | import maya.api.OpenMayaAnim as omAnim 7 | import maya.api.OpenMaya as om 8 | import imath 9 | import array 10 | import zlib 11 | import json 12 | import maya.cmds as cmds 13 | from typing import List 14 | import time 15 | import struct 16 | import uuid 17 | import xgenm as xg 18 | import os 19 | import itertools 20 | 21 | _XGenExporterVersion = "1.09" 22 | print_debug = False 23 | 24 | 25 | # %% 26 | def list2ImathArray(l: list, _type): 27 | arr = _type(len(l)) 28 | for i in range(len(l)): 29 | arr[i] = l[i] 30 | return arr 31 | 32 | 33 | def floatList2V3fArray(l: list): 34 | arr = imath.V3fArray(len(l) // 3) 35 | for i in range(len(arr)): 36 | arr[i].x = l[i * 3] 37 | arr[i].y = l[i * 3 + 1] 38 | arr[i].z = l[i * 3 + 2] 39 | return arr 40 | 41 | 42 | # %% 43 | def getXgenData(fnDepNode: om.MFnDependencyNode, keys): 44 | splineData: om.MPlug = fnDepNode.findPlug("outSplineData", False) 45 | 46 | handle: om.MDataHandle = splineData.asMObject() 47 | mdata = om.MFnPluginData(handle) 48 | mData = mdata.data() 49 | 50 | rawData = mData.writeBinary() 51 | 52 | def GetBlocks(bype_data): 53 | address = 0 54 | i = 0 55 | blocks = [] 56 | maxIt = 100 57 | while address < len(bype_data) - 1: 58 | size = int.from_bytes(bype_data[address + 8:address + 16], byteorder='little', signed=False) 59 | type_code = int.from_bytes(bype_data[address:address + 4], byteorder='little', signed=False) 60 | blocks.append((address + 16, address + 16 + size, type_code)) 61 | address += size + 16 62 | i += 1 63 | if i > maxIt: 64 | break 65 | return blocks 66 | 67 | dataBlocks = GetBlocks(rawData) 68 | headerBlock = dataBlocks[0] 69 | dataBlocks.pop(0) 70 | 71 | dataJson = json.loads(rawData[headerBlock[0]:headerBlock[1]]) 72 | # print(dataJson) 73 | Header = dataJson['Header'] 74 | 75 | Items = dict() 76 | 77 | def readItems(items): 78 | for k, v in items: 79 | if isinstance(v, int): 80 | group = v >> 32 81 | index = v & 0xFFFFFFFF 82 | addr = (group, index) 83 | if k not in Items: 84 | Items[k] = [addr] 85 | else: 86 | Items[k].append(addr) 87 | 88 | for i in range(len(dataJson['Items'])): 89 | readItems(dataJson['Items'][i].items()) 90 | for i in range(len(dataJson['RefMeshArray'])): 91 | readItems(dataJson['RefMeshArray'][i].items()) 92 | 93 | # print(Items) 94 | decompressedData = dict() 95 | 96 | def decompressData(group, index): 97 | if group not in decompressedData: 98 | if Header['GroupBase64']: 99 | raise Exception("我还没有碰到Base64的情况,请提醒我更新代码") 100 | if Header['GroupDeflate']: 101 | validData = zlib.decompress(rawData[dataBlocks[group][0] + 32:]) 102 | else: 103 | validData = rawData[dataBlocks[group][0]:dataBlocks[group][1]] 104 | decompressedData[group] = validData 105 | else: 106 | validData = decompressedData[group] 107 | blocks = GetBlocks(validData) 108 | return validData[blocks[index][0]:blocks[index][1]] 109 | 110 | outputs = {key: [] for key in keys} 111 | for k, v in Items.items(): 112 | if k not in outputs: 113 | continue 114 | if k == 'PrimitiveInfos': 115 | dtype_format = ' 1: 233 | knotsLength = len(knotsArray) 234 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 235 | knotsArray[0] == knotsArray[1]): 236 | knots.append(float(knotsArray[0])) 237 | else: 238 | knots.append(float(2 * knotsArray[0] - knotsArray[1])) 239 | 240 | for j in range(knotsLength): 241 | knots.append(float(knotsArray[j])) 242 | 243 | if (knotsArray[0] == knotsArray[knotsLength - 1] or 244 | knotsArray[knotsLength - 1] == knotsArray[knotsLength - 2]): 245 | knots.append(float(knotsArray[knotsLength - 1])) 246 | else: 247 | knots.append(float(2 * knotsArray[knotsLength - 1] - knotsArray[knotsLength - 2])) 248 | samp.setCurvesNumVertices(nVertices) 249 | samp.setPositions(floatList2V3fArray(pointslist)) 250 | samp.setOrders(list2ImathArray(orders, imath.UnsignedCharArray)) 251 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 252 | 253 | # widths = list2ImathArray([0.1], imath.FloatArray) 254 | # widths = abc.Float32TPTraits() 255 | # widths = abcGeom.OFloatGeomParamSample(widths, abcGeom.GeometryScope.kConstantScope) 256 | # samp.setWidths(widths) 257 | self.schema.set(samp) 258 | 259 | def write_frame(self): 260 | numCurves = len(self.curves) 261 | if numCurves == 0: 262 | return 263 | curve = om.MFnNurbsCurve(self.curves[0]) 264 | 265 | samp = abcGeom.OCurvesSchemaSample() 266 | samp.setBasis(self.firstSamp.getBasis()) 267 | samp.setWrap(self.firstSamp.getWrap()) 268 | samp.setType(self.firstSamp.getType()) 269 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 270 | samp.setOrders(self.firstSamp.getOrders()) 271 | samp.setKnots(self.firstSamp.getKnots()) 272 | 273 | pointslist = [] 274 | for i in range(numCurves): 275 | curve = curve.setObject(self.curves[i]) 276 | numCVs = curve.numCVs 277 | cvArray = curve.cvPositions() 278 | for j in range(numCVs): 279 | pointslist.append(cvArray[j].x) 280 | pointslist.append(cvArray[j].y) 281 | pointslist.append(cvArray[j].z) 282 | 283 | samp.setPositions(floatList2V3fArray(pointslist)) 284 | 285 | self.schema.set(samp) 286 | 287 | def bake_uv(self, bakeMesh: om.MFnMesh, uv_set: str = None): 288 | if not self.needBakeUV or self.hairRootList is None: 289 | return 290 | if bakeMesh is None: 291 | return 292 | if uv_set is None: 293 | uv_set = bakeMesh.currentUVSetName() 294 | elif uv_set not in bakeMesh.getUVSetNames(): 295 | raise Exception(f'Invalid UV Set : {uv_set}') 296 | 297 | uvs = imath.V2fArray(len(self.hairRootList)) 298 | for i, hairRoot in enumerate(self.hairRootList): 299 | res = bakeMesh.getUVAtPoint(hairRoot, om.MSpace.kWorld, uvSet=uv_set) 300 | uvs[i].x = res[0] 301 | uvs[i].y = res[1] 302 | 303 | self.write_param('groom_root_uv', AbcType.vector2f, uvs) 304 | 305 | 306 | # %% 307 | try: 308 | from PySide6 import QtCore, QtWidgets, QtGui 309 | import shiboken6 as shiboken 310 | except: 311 | from PySide2 import QtCore, QtWidgets, QtGui 312 | import shiboken2 as shiboken 313 | 314 | import maya.OpenMayaUI as om1ui 315 | 316 | 317 | def mayaWindow(): 318 | main_window_ptr = om1ui.MQtUtil.mainWindow() 319 | return shiboken.wrapInstance(int(main_window_ptr), QtWidgets.QWidget) 320 | 321 | 322 | class FileSelectorWidget(QtWidgets.QWidget): 323 | def __init__(self, callback): 324 | super(FileSelectorWidget, self).__init__() 325 | self.callback = callback 326 | self.setup_ui() 327 | 328 | def setup_ui(self): 329 | self.layout = QtWidgets.QHBoxLayout(self) 330 | self.file_line_edit = QtWidgets.QLineEdit(self) 331 | self.layout.addWidget(self.file_line_edit) 332 | self.browse_button = QtWidgets.QPushButton("Browse", self) 333 | self.layout.addWidget(self.browse_button) 334 | self.browse_button.clicked.connect(self.browse_file) 335 | self.file_line_edit.textChanged.connect(self.callback) 336 | 337 | def browse_file(self): 338 | file_path = cmds.fileDialog2(fileMode=1, caption="Select a File") 339 | if file_path: 340 | self.file_line_edit.setText(file_path[0]) 341 | 342 | def set_file_path(self, path): 343 | return self.file_line_edit.setText(path) 344 | 345 | def get_file_path(self): 346 | return self.file_line_edit.text() 347 | 348 | 349 | # %% 350 | SaveXGenDesWindowParentName = "_saveXGenDesWindow" 351 | 352 | 353 | def getSaveXGenDesWindowParent(): 354 | sel = om.MSelectionList() 355 | try: 356 | sel.add(SaveXGenDesWindowParentName) 357 | except: 358 | trans = om.MFnTransform() 359 | trans.create() 360 | trans.setName(SaveXGenDesWindowParentName) 361 | sel.add(SaveXGenDesWindowParentName) 362 | return sel.getDagPath(0) 363 | 364 | 365 | def deleteSaveXGenDesWindowParent(): 366 | if cmds.objExists(SaveXGenDesWindowParentName): 367 | cmds.delete(SaveXGenDesWindowParentName) 368 | 369 | 370 | # %% 371 | 372 | 373 | def getExpressionPath(expr: str, pal_path, des_path, fx_name): 374 | expr = expr.replace('${DESC}', xg.descriptionPath(pal_path, des_path)).replace('${FXMODULE}', fx_name) 375 | ptex_path = None 376 | if os.path.isdir(expr): 377 | for f in os.listdir(expr): 378 | if f.endswith('.ptx'): 379 | ptex_path = f 380 | 381 | if ptex_path is None: 382 | return "" 383 | return os.path.normpath(os.path.join(expr, ptex_path)) 384 | 385 | 386 | def getClumpingPtexPath(dn: om.MFnDependencyNode): 387 | if not dn.object().hasFn(om.MFn.kTransform): 388 | des_obj = om.MFnDagNode(dn.object()).parent(0) 389 | else: 390 | des_obj = dn.object() 391 | des_path = str(om.MDagPath.getAPathTo(des_obj)) 392 | pal_path = str(om.MDagPath.getAPathTo(om.MFnDagNode(des_obj).parent(0))) 393 | clumping = None 394 | for fx in xg.fxModules(pal_path, des_path): 395 | if fx.startswith('Clumping'): 396 | clumping = fx 397 | break 398 | if clumping is None: 399 | return "" 400 | expr = xg.getAttr("mapDir", pal_path, des_path, clumping) 401 | return getExpressionPath(expr, pal_path, des_path, clumping) 402 | 403 | 404 | # %% 405 | def generate_short_hash(): 406 | unique_id = uuid.uuid4() 407 | return str(unique_id).replace('-', '')[:8] 408 | 409 | 410 | def ConvertToInteractive(dn: om.MFnDependencyNode): 411 | if not dn.object().hasFn(om.MFn.kTransform): 412 | path = om.MDagPath.getAPathTo(om.MFnDagNode(dn.object()).parent(0)) 413 | else: 414 | path = om.MDagPath.getAPathTo(dn.object()) 415 | cmds.select(path, replace=True) 416 | prefix = "z" + generate_short_hash() 417 | res = cmds.xgmGroomConvert(prefix=prefix) 418 | if res is None or len(res) == 0: 419 | raise Exception("Convert to interactive failed.") 420 | sel: om.MSelectionList = om.MGlobal.getActiveSelectionList() 421 | 422 | if not res[0].startswith(prefix): 423 | fnDepNode = om.MFnDependencyNode(sel.getDependNode(0)) 424 | if not fnDepNode.object().hasFn(om.MFn.kTransform): 425 | fnDepNode = om.MFnDependencyNode(om.MFnDagNode(fnDepNode.object()).parent(0)) 426 | fnDepNode.setName('_'.join((prefix, fnDepNode.name()))) 427 | 428 | spline = om.MFnDagNode(sel.getDagPath(0)) 429 | om.MFnDagNode(getSaveXGenDesWindowParent()).addChild(spline.parent(0)) 430 | return spline 431 | # return curve.parent(0) 432 | 433 | 434 | # %% 435 | import ctypes 436 | from ctypes import c_void_p, c_uint64, c_ulonglong, c_float, c_int, c_char_p 437 | 438 | PtexSamplerDllFuncName = "_PtexSamplerDllFunc" 439 | 440 | 441 | class PtexSampler: 442 | class DllFunc: 443 | @staticmethod 444 | def getVFunc(obj, index, *args): 445 | vtble = ctypes.cast(obj, ctypes.POINTER(ctypes.c_void_p)).contents 446 | vfuncAddr = ctypes.cast(vtble.value + index * 8, ctypes.POINTER(ctypes.c_void_p)).contents.value 447 | return ctypes.CFUNCTYPE(*args)(vfuncAddr) 448 | 449 | @staticmethod 450 | def getPtexFilterEvelFunc(filter): 451 | ptexFilterEvel = PtexSampler.DllFunc.getVFunc(filter, 2, c_void_p, c_void_p, c_void_p, c_int, c_int, c_int, 452 | c_float, c_float, 453 | c_float, c_float, c_float, c_float) 454 | return ptexFilterEvel 455 | 456 | @staticmethod 457 | def getPtexTextureReleaseFunc(ptexTexture): 458 | ptexTextureRelease = PtexSampler.DllFunc.getVFunc(ptexTexture, 1, c_void_p) 459 | return ptexTextureRelease 460 | 461 | class MyVector(ctypes.Structure): 462 | _fields_ = [ 463 | ("_Myfirst", ctypes.POINTER(ctypes.c_float)), # pointer to beginning of array 464 | ("_Mylast", ctypes.POINTER(ctypes.c_float)), # pointer to current end of sequence 465 | ("_Myend", ctypes.POINTER(ctypes.c_float)) # pointer to end of sequence 466 | ] 467 | 468 | def __init__(self, size=10): 469 | array = (ctypes.c_float * size)() 470 | # 为每个指针分配内存 471 | self._Myfirst = ctypes.cast(array, ctypes.POINTER(ctypes.c_float)) 472 | # _Mylast 初始化为数组的开始(指向和 _Myfirst 相同的位置) 473 | self._Mylast = self._Myfirst # 当前结束位置指向数组的开始 474 | _myend_address = ctypes.addressof(array) + ctypes.sizeof(array) # 获取数组末尾(地址) 475 | self._Myend = ctypes.cast(_myend_address, ctypes.POINTER(ctypes.c_float)) 476 | 477 | def __getitem__(self, index): 478 | return self._Myfirst[index] 479 | 480 | def close(self): 481 | getPtexTextureRelease = PtexSampler.DllFunc.getPtexTextureReleaseFunc(self.ptexTexture) 482 | getPtexTextureRelease(self.ptexTexture) 483 | 484 | def setupFilter(self, path): 485 | DllFunc: PtexSampler.DllFunc = globals()[PtexSamplerDllFuncName] 486 | err = ctypes.c_uint64() 487 | ptexTexture = DllFunc.ptex_open( 488 | path.encode(), 489 | ctypes.byref(err), 0) 490 | if ptexTexture is None: 491 | raise Exception("no ptexTexture found") 492 | self.ptexTexture = ctypes.c_void_p(ptexTexture) 493 | 494 | # 定义Options结构体 495 | class Options(ctypes.Structure): 496 | _fields_ = [ 497 | ("__structSize", ctypes.c_int), # (for internal use only) 498 | ("filter", ctypes.c_int), # Filter type. 499 | ("lerp", ctypes.c_bool), # Interpolate between mipmap levels. 500 | ("sharpness", ctypes.c_float), # Filter sharpness, 0..1 (for general bi-cubic filter only). 501 | ("noedgeblend", ctypes.c_bool) # Disable cross-face filtering. 502 | ] 503 | 504 | def __init__(self, filter_=0, lerp_=False, sharpness_=0.0, 505 | noedgeblend_=False): # Point-sampled (no filtering) 506 | self.__structSize = ctypes.sizeof(Options) # 设置结构体大小 507 | self.filter = filter_ # 设置过滤器类型 508 | self.lerp = lerp_ # 设置是否插值 509 | self.sharpness = sharpness_ # 设置过滤器锐度 510 | self.noedgeblend = noedgeblend_ # 设置是否禁用跨面过滤 511 | 512 | options = Options() 513 | filter = DllFunc.ptex_getFilter(self.ptexTexture, options) 514 | filter = ctypes.cast(filter, ctypes.c_void_p) 515 | self.ptexFilterEvalFunc = DllFunc.getPtexFilterEvelFunc(filter) 516 | self.vector = DllFunc.temp_vector 517 | self.filter = filter 518 | 519 | def __init__(self, path: str): 520 | self.path = path 521 | if PtexSamplerDllFuncName in globals(): 522 | self.setupFilter(path) 523 | return 524 | 525 | DllFunc = PtexSampler.DllFunc() 526 | globals()[PtexSamplerDllFuncName] = DllFunc 527 | 528 | ptex_dll = ctypes.cdll.LoadLibrary("Ptex.dll") 529 | versions = ['2_2', '2_3', '2_4', '2_5', '2_6'] 530 | version = None 531 | for _v in versions: 532 | try: 533 | Ptex_String_release = ptex_dll[f"??1String@v{_v}@Ptex@@QEAA@XZ"] 534 | version = _v 535 | break 536 | except: 537 | pass 538 | if version is None: 539 | raise Exception("Could not find correct Ptex version") 540 | 541 | DllFunc.ptex_open = ptex_dll[f"?open@PtexTexture@v{version}@Ptex@@SAPEAV123@PEBDAEAVString@23@_N@Z"] 542 | DllFunc.ptex_open.restype = ctypes.c_void_p 543 | DllFunc.ptex_getFilter = ptex_dll[ 544 | f'?getFilter@PtexFilter@v{version}@Ptex@@SAPEAV123@PEAVPtexTexture@23@AEBUOptions@123@@Z'] 545 | DllFunc.ptex_getFilter.restype = ctypes.c_void_p 546 | DllFunc.temp_vector = DllFunc.MyVector(10) 547 | self.setupFilter(path) 548 | 549 | def sampleData(self, faceU, faceV, faceId): 550 | self.ptexFilterEvalFunc(self.filter, self.vector._Myfirst, 0, 3, faceId, faceU, faceV, 0, 0, 0, 0) 551 | return self.vector[:3] 552 | 553 | 554 | # %% 555 | class XGenProxyEveryFrame(CurvesProxy): 556 | def __init__(self, curveObj: abcGeom.OCurves, descFnDepNode: om.MFnDependencyNode, needRootList=False, 557 | animation=False): 558 | super().__init__(curveObj, None, needRootList, animation) 559 | self.descFnDepNode = descFnDepNode 560 | self.sorted_offset_map = None 561 | 562 | def write_first_frame(self): 563 | if print_debug: 564 | startTime = time.time() 565 | 566 | spline = ConvertToInteractive(self.descFnDepNode) 567 | self.fnDepNode = spline 568 | self.firstSpline = spline 569 | PrimitiveInfosList, PositionsDataList, WidthsDataList, FaceIdList, FaceUVList = getXgenData(self.fnDepNode, 570 | ('PrimitiveInfos', 571 | 'Positions', 572 | 'WIDTH_CV', 573 | 'FaceId', 574 | 'FaceUV')) 575 | # calculate sorted order first 576 | index2order = self.get_index2order(FaceIdList, FaceUVList) 577 | self.firstSplineIndex2order = index2order 578 | if print_debug: 579 | print("getXgenData: %.4f" % (time.time() - startTime)) 580 | startTime = time.time() 581 | numCurves = 0 582 | numCVs = 0 583 | for i, PrimitiveInfos in enumerate(PrimitiveInfosList): 584 | numCurves += len(PrimitiveInfos) 585 | for PrimitiveInfo in PrimitiveInfos: 586 | length = int(PrimitiveInfo[1]) 587 | if length < 2: 588 | continue 589 | numCVs += length 590 | self.numCurves = numCurves 591 | self.numCVs = numCVs 592 | orders = imath.UnsignedCharArray(numCurves) 593 | nVertices = imath.IntArray(numCurves) 594 | 595 | samp = self.firstSamp 596 | samp.setBasis(abcGeom.BasisType.kBsplineBasis) 597 | samp.setWrap(abcGeom.CurvePeriodicity.kNonPeriodic) 598 | samp.setType(abcGeom.CurveType.kCubic) 599 | 600 | degree = 3 601 | pointArray = imath.V3fArray(numCVs) 602 | widthArray = imath.FloatArray(numCVs) 603 | if self.needRootList: 604 | self.hairRootList = [None] * numCurves 605 | knots = [None] * numCurves 606 | 607 | curveIndex = 0 608 | # cvIndex = 0 609 | # cvOffsets = imath.IntArray(numCurves) 610 | curvelengths = imath.IntArray(numCurves) 611 | 612 | for j in range(len(PrimitiveInfosList)): 613 | PrimitiveInfos = PrimitiveInfosList[j] 614 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 615 | length = int(PrimitiveInfo[1]) 616 | if length < 2: 617 | continue 618 | curvelengths[index2order[curveIndex]] = length 619 | curveIndex += 1 620 | 621 | self.sorted_offset_map = [0] + list(itertools.accumulate(curvelengths)) 622 | 623 | curveIndex = 0 624 | # cvOffsets = imath.IntArray(numCurves) 625 | for j in range(len(PrimitiveInfosList)): 626 | PrimitiveInfos = PrimitiveInfosList[j] 627 | posData = PositionsDataList[j] 628 | widthData = WidthsDataList[j] 629 | for i, PrimitiveInfo in enumerate(PrimitiveInfos): 630 | offset = PrimitiveInfo[0] 631 | length = int(PrimitiveInfo[1]) 632 | if length < 2: 633 | continue 634 | startAddr = offset * 3 635 | sortedCurveIndex = index2order[curveIndex] 636 | # cvOffsets[curveIndex] = cvIndex 637 | cvIndex = self.sorted_offset_map[sortedCurveIndex] 638 | for k in range(length): 639 | pointArray[cvIndex].x = posData[startAddr] 640 | pointArray[cvIndex].y = posData[startAddr + 1] 641 | pointArray[cvIndex].z = posData[startAddr + 2] 642 | if k == 0 and self.needRootList: 643 | self.hairRootList[sortedCurveIndex] = om.MPoint(pointArray[cvIndex]) 644 | widthArray[cvIndex] = widthData[offset + k] 645 | startAddr += 3 646 | cvIndex += 1 647 | 648 | orders[sortedCurveIndex] = degree + 1 649 | nVertices[sortedCurveIndex] = length 650 | 651 | knotsInsideNum = length - degree + 1 652 | knotsList = [*([0] * degree), *list(range(knotsInsideNum)), 653 | *([knotsInsideNum - 1] * degree)] # The endpoint repeats one more than Maya 654 | # print(knotsList) 655 | knots[sortedCurveIndex] = knotsList 656 | curveIndex += 1 657 | knots = list(itertools.chain(*knots)) 658 | 659 | samp.setCurvesNumVertices(nVertices) 660 | samp.setPositions(pointArray) 661 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 662 | samp.setOrders(orders) 663 | 664 | widths = abcGeom.OFloatGeomParamSample(widthArray, abcGeom.GeometryScope.kVertexScope) 665 | samp.setWidths(widths) 666 | self.schema.set(samp) 667 | # if self.animation: 668 | # index2order = self.get_index2order(FaceIdList, FaceUVList) 669 | # self.order_offset_map = imath.IntArray(numCurves) 670 | # for i, offset in zip(index2order, cvOffsets): 671 | # self.order_offset_map[i] = offset 672 | if print_debug: 673 | # print(self.order_offset_map) 674 | print("write_first_frame: %.4f" % (time.time() - startTime)) 675 | 676 | @staticmethod 677 | def get_index2order(FaceIdList, FaceUVList): 678 | order_list = [] 679 | for j in range(len(FaceIdList)): 680 | FaceUVData = FaceUVList[j] 681 | FaceIdData = FaceIdList[j] 682 | for i, faceId in enumerate(FaceIdData): 683 | u = FaceUVData[i * 2] 684 | v = FaceUVData[i * 2 + 1] 685 | order_list.append((faceId, u, v)) 686 | sorted_list = sorted((key, i) for i, key in enumerate(order_list)) 687 | index_list = imath.IntArray(len(order_list)) 688 | for order_index, item in enumerate(sorted_list): 689 | my_index = item[1] 690 | index_list[my_index] = order_index 691 | # if print_debug: 692 | # print(sorted_list) 693 | return index_list 694 | 695 | def write_frame(self): 696 | if print_debug: 697 | startTime = time.time() 698 | spline = ConvertToInteractive(self.descFnDepNode) 699 | self.fnDepNode = spline 700 | PrimitiveInfosList, PositionsDataList, FaceIdList, FaceUVList = getXgenData(self.fnDepNode, ('PrimitiveInfos', 701 | 'Positions', 702 | 'FaceId', 703 | 'FaceUV')) 704 | 705 | numCVs = self.numCVs 706 | 707 | samp = abcGeom.OCurvesSchemaSample() 708 | samp.setBasis(self.firstSamp.getBasis()) 709 | samp.setWrap(self.firstSamp.getWrap()) 710 | samp.setType(self.firstSamp.getType()) 711 | 712 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 713 | samp.setKnots(self.firstSamp.getKnots()) 714 | samp.setOrders(self.firstSamp.getOrders()) 715 | samp.setWidths(self.firstSamp.getWidths()) 716 | 717 | if print_debug: 718 | s = time.time() 719 | index2order = self.get_index2order(FaceIdList, FaceUVList) 720 | 721 | pointArray = imath.V3fArray(numCVs) 722 | 723 | curveIndex = 0 724 | for j in range(len(PrimitiveInfosList)): 725 | PrimitiveInfos = PrimitiveInfosList[j] 726 | posData = PositionsDataList[j] 727 | for PrimitiveInfo in PrimitiveInfos: 728 | offset = PrimitiveInfo[0] 729 | length = int(PrimitiveInfo[1]) 730 | if length < 2: 731 | continue 732 | startAddr = offset * 3 733 | cvIndex = self.sorted_offset_map[index2order[curveIndex]] 734 | for k in range(length): 735 | pointArray[cvIndex].x = posData[startAddr] 736 | pointArray[cvIndex].y = posData[startAddr + 1] 737 | pointArray[cvIndex].z = posData[startAddr + 2] 738 | startAddr += 3 739 | cvIndex += 1 740 | 741 | curveIndex += 1 742 | if print_debug: 743 | print("loop: %.4f" % (time.time() - s)) 744 | samp.setPositions(pointArray) 745 | 746 | self.schema.set(samp) 747 | if print_debug: 748 | print("write_frame: %.4f" % (time.time() - startTime)) 749 | 750 | 751 | # %% 752 | 753 | GroomGuideIdStartIndexName = '_GroomGuideIdStartIndexName' 754 | 755 | 756 | def getGroomGuideIdStartIndex(): 757 | return globals()[GroomGuideIdStartIndexName] 758 | 759 | 760 | def setGroomGuideIdStartIndex(value=0): 761 | globals()[GroomGuideIdStartIndexName] = value 762 | 763 | 764 | # %% 765 | class GuideProxy(CurvesProxy): 766 | def __init__(self, curveObj: abcGeom.OCurves, fnDepNode: om.MFnDependencyNode, needRootList=False, animation=False): 767 | if not fnDepNode.object().hasFn(om.MFn.kTransform): 768 | fnDepNode = om.MFnDependencyNode(om.MFnDagNode(fnDepNode.object()).parent(0)) 769 | super().__init__(curveObj, fnDepNode, needRootList, animation) 770 | itDag = om.MItDag() 771 | itDag.reset(self.fnDepNode.object(), om.MItDag.kBreadthFirst, om.MFn.kInvalid) 772 | guides = [] 773 | while not itDag.isDone(): 774 | dn = om.MFnDependencyNode(itDag.currentItem()) 775 | if dn.typeName == 'xgmSplineGuide': 776 | guides.append(itDag.getPath()) 777 | itDag.next() 778 | self.guides: list[om.MDagPath] = guides 779 | self.xgenProxy = None 780 | self.ptexPath = None 781 | self.writePtexGuideId = False 782 | 783 | def set_xgen_proxy_and_ptex(self, xgenSpline: XGenProxyEveryFrame, ptexPath: str): 784 | self.xgenProxy = xgenSpline 785 | self.ptexPath = ptexPath 786 | 787 | def write_guide_id_from_ptex(self): 788 | if not self.writePtexGuideId: 789 | return 790 | ptexPath = self.ptexPath 791 | xgenSpline = self.xgenProxy 792 | if ptexPath is None or ptexPath == "": 793 | return 794 | if self.xgenProxy == None: 795 | return 796 | ptexSampler = PtexSampler(ptexPath) 797 | self.regionPtex = ptexPath 798 | 799 | guide_map = dict() 800 | 801 | def color2Int(color): 802 | a = int(color[0] * 255) & 0xff 803 | b = int(color[1] * 255) & 0xff 804 | c = int(color[2] * 255) & 0xff 805 | return (a << 16) | (b << 8) | c 806 | 807 | for i, guide in enumerate(self.guides): 808 | dn = om.MFnDependencyNode(guide.node()) 809 | u = dn.findPlug('uLoc', False).asFloat() 810 | v = dn.findPlug('vLoc', False).asFloat() 811 | faceId = dn.findPlug('faceId', False).asInt() 812 | color = ptexSampler.sampleData(u, v, faceId) 813 | hash = color2Int(color) 814 | if hash in guide_map: 815 | old_guide = om.MFnDependencyNode(guide_map[hash][0].node()) 816 | print( 817 | f"guide {dn.name()} and {old_guide.name()} are in the same area on texture, only use {old_guide.name()}.") 818 | continue 819 | guide_map[hash] = (guide, i) 820 | 821 | FaceUVList, FaceIdList = getXgenData(xgenSpline.firstSpline, ('FaceUV', 'FaceId')) 822 | 823 | guideIdStartIndex = getGroomGuideIdStartIndex() 824 | guideIdNextStartIndex = guideIdStartIndex + len(self.guides) 825 | groom_id_data = list2ImathArray(list(range(guideIdStartIndex, guideIdNextStartIndex)), imath.IntArray) 826 | self.write_param("groom_id", AbcType.int32, groom_id_data) 827 | 828 | spline_num = len(xgenSpline.hairRootList) 829 | weight_data = list2ImathArray([1.0] * spline_num, imath.FloatArray) 830 | xgenSpline.write_param("groom_guide_weights", AbcType.float, weight_data) 831 | 832 | guide_id_data = imath.IntArray(spline_num) 833 | first_guide_name = om.MFnDependencyNode(self.guides[0].node()).name() 834 | spline_index = 0 835 | for j in range(len(FaceIdList)): 836 | FaceUVData = FaceUVList[j] 837 | FaceIdData = FaceIdList[j] 838 | # print(len(FaceIdData),len(FaceUVData)) 839 | for i, faceId in enumerate(FaceIdData): 840 | u = FaceUVData[i * 2] 841 | v = FaceUVData[i * 2 + 1] 842 | color = ptexSampler.sampleData(u, v, faceId) 843 | hash = color2Int(color) 844 | guide_id = guideIdStartIndex 845 | if hash not in guide_map: 846 | print(f"The spline index ({j} ,{i}) does not have a valid guide attached to {first_guide_name}.") 847 | else: 848 | guide_id += guide_map[hash][1] 849 | 850 | if spline_index >= spline_num: 851 | raise Exception("spline_index >= spline_num") 852 | sorted_spine_index = xgenSpline.firstSplineIndex2order[spline_index] 853 | guide_id_data[sorted_spine_index] = guide_id 854 | # guide_map[hash][2].append(spline_index) 855 | spline_index += 1 856 | # print(guide_map) 857 | ptexSampler.close() 858 | xgenSpline.write_param("groom_closest_guides", AbcType.int32, guide_id_data) 859 | setGroomGuideIdStartIndex(guideIdNextStartIndex) 860 | 861 | def write_first_frame(self): 862 | numCurves = len(self.guides) 863 | orders = imath.IntArray(numCurves) 864 | nVertices = imath.IntArray(numCurves) 865 | pointslist = [] 866 | knots = [] 867 | if self.needRootList: 868 | self.hairRootList = [] 869 | 870 | samp = self.firstSamp 871 | samp.setBasis(abcGeom.BasisType.kBsplineBasis) 872 | samp.setWrap(abcGeom.CurvePeriodicity.kNonPeriodic) 873 | samp.setType(abcGeom.CurveType.kLinear) 874 | degree = 1 875 | 876 | for i in range(numCurves): 877 | data = cmds.xgmGuideGeom(guide=self.guides[i], numVertices=True) 878 | numCVs = int(data[0]) 879 | data = cmds.xgmGuideGeom(guide=self.guides[i], controlPoints=True) 880 | pointslist += data 881 | orders[i] = degree + 1 882 | nVertices[i] = numCVs 883 | if self.needRootList: 884 | self.hairRootList.append(om.MPoint(data[:3])) 885 | 886 | knotsInsideNum = numCVs - degree + 1 887 | knotsList = [*([0] * degree), *list(range(knotsInsideNum)), 888 | *([knotsInsideNum - 1] * degree)] # The endpoint repeats one more than Maya 889 | # print(knotsList) 890 | knots += knotsList 891 | samp.setCurvesNumVertices(nVertices) 892 | samp.setPositions(floatList2V3fArray(pointslist)) 893 | samp.setOrders(list2ImathArray(orders, imath.UnsignedCharArray)) 894 | samp.setKnots(list2ImathArray(knots, imath.FloatArray)) 895 | self.schema.set(samp) 896 | 897 | def write_frame(self): 898 | numCurves = len(self.guides) 899 | if numCurves == 0: 900 | return 901 | 902 | samp = abcGeom.OCurvesSchemaSample() 903 | samp.setBasis(self.firstSamp.getBasis()) 904 | samp.setWrap(self.firstSamp.getWrap()) 905 | samp.setType(self.firstSamp.getType()) 906 | samp.setCurvesNumVertices(self.firstSamp.getCurvesNumVertices()) 907 | samp.setOrders(self.firstSamp.getOrders()) 908 | samp.setKnots(self.firstSamp.getKnots()) 909 | 910 | pointslist = [] 911 | for i in range(numCurves): 912 | data = cmds.xgmGuideGeom(guide=self.guides[i], controlPoints=True) 913 | pointslist += data 914 | 915 | samp.setPositions(floatList2V3fArray(pointslist)) 916 | self.schema.set(samp) 917 | 918 | 919 | # %% 920 | 921 | class SaveXGenDesWindow(QtWidgets.QDialog): 922 | class MultiSelectCheckBox(QtWidgets.QCheckBox): 923 | def __init__(self, column_name, parent=None): 924 | super().__init__(parent) 925 | self.column_name = column_name 926 | self.clicked.connect(self.on_clicked) 927 | 928 | def on_clicked(self, checked): 929 | window = self.find_window() 930 | if not window or not hasattr(window, 'table'): 931 | return 932 | table = window.table 933 | contents = window.contentList 934 | selected_rows = self.get_rows_to_changing(table) 935 | for row in selected_rows: 936 | if 0 <= row < len(contents): 937 | content = contents[row] 938 | checkbox = getattr(content, self.column_name) 939 | if checkbox is not None: 940 | checkbox.blockSignals(True) 941 | checkbox.setChecked(checked) 942 | checkbox.blockSignals(False) 943 | 944 | def find_window(self): 945 | parent = self.parent() 946 | while parent: 947 | if (isinstance(parent, SaveXGenDesWindow) or 948 | parent.__class__.__name__ == 'SaveXGenDesWindow'): 949 | return parent 950 | parent = parent.parent() 951 | return None 952 | 953 | def get_rows_to_changing(self, table): 954 | pos = self.mapTo(table.viewport(), QtCore.QPoint(0, 0)) 955 | _index = table.indexAt(pos).row() 956 | selected_rows = [index.row() for index in table.selectionModel().selectedRows()] 957 | return selected_rows if _index in selected_rows else [_index] 958 | 959 | class Content: 960 | def __init__(self, fnDepNode, showName, groupName, useGuide, bakeUV, animation, export): 961 | self.showName = showName 962 | self.fnDepNode = fnDepNode 963 | self.groupName = QtWidgets.QLineEdit() 964 | self.groupName.setText(groupName) 965 | self.useGuide = SaveXGenDesWindow.MultiSelectCheckBox("useGuide") 966 | self.useGuide.setChecked(useGuide) 967 | self.bakeUV = SaveXGenDesWindow.MultiSelectCheckBox("bakeUV") 968 | self.bakeUV.setChecked(bakeUV) 969 | self.animation = SaveXGenDesWindow.MultiSelectCheckBox("animation") 970 | self.animation.setChecked(animation) 971 | self.export = SaveXGenDesWindow.MultiSelectCheckBox("export") 972 | self.export.setChecked(export) 973 | self.splineAnimation = False 974 | self.writePtexGuideId = False 975 | self.regionPtex = "" 976 | 977 | def __init__(self, parent=mayaWindow()): 978 | super(SaveXGenDesWindow, self).__init__(parent) 979 | self.contentList: List[SaveXGenDesWindow.Content] = [] 980 | self.save_path = '.' 981 | self.bakeMesh = None 982 | self.setWindowTitle("Export XGen description to UE Groom v{}".format(_XGenExporterVersion)) 983 | self.setGeometry(400, 400, 1130, 550) 984 | self.buildUI() 985 | 986 | def showAbout(self): 987 | QtWidgets.QMessageBox.about(self, "Export XGen to UE Groom", 988 | "A small tool to export XGen to UE Groom, by PDE26jjk. Link: https://github.com/PDE26jjk/XGenUEGroomExporter") 989 | 990 | def createFrame(self, labelText): 991 | try: 992 | frame = om1ui.MQtUtil.findControl( 993 | cmds.frameLayout(label=labelText, collapsable=True, collapse=True, manage=True)) 994 | frame: QtWidgets.QWidget = shiboken.wrapInstance(int(frame), QtWidgets.QWidget) 995 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 996 | frameLayout: QtWidgets.QLayout = frame.children()[2].children()[0] 997 | except: 998 | frame = QtWidgets.QFrame(self) 999 | frame.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Maximum) 1000 | frameLayout = QtWidgets.QVBoxLayout(frame) 1001 | frame.children().append(frameLayout) 1002 | return frame, frameLayout 1003 | 1004 | def buildUI(self): 1005 | main_layout = QtWidgets.QVBoxLayout() 1006 | 1007 | menu_bar = QtWidgets.QMenuBar(self) 1008 | menu_bar.addMenu("Help").addAction("About", self.showAbout) 1009 | main_layout.setMenuBar(menu_bar) 1010 | 1011 | label1 = QtWidgets.QLabel("Select XGen Description") 1012 | label1.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 1013 | hBox = QtWidgets.QHBoxLayout() 1014 | hBox.setContentsMargins(10, 4, 10, 4) 1015 | hBox.addWidget(label1) 1016 | 1017 | self.fillWithSelectList_button = QtWidgets.QPushButton("Refresh selected") 1018 | self.fillWithSelectList_button.clicked.connect(self.fillTableWithSelectList) 1019 | self.fillWithSelectList_button.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 1020 | # main_layout.addWidget(self.fillWithSelectList_button) 1021 | hBox.addStretch(1) 1022 | hBox.addWidget(self.fillWithSelectList_button) 1023 | main_layout.addLayout(hBox) 1024 | 1025 | self.table = QtWidgets.QTableWidget(self) 1026 | self.table.setColumnCount(7) 1027 | self.table.setHorizontalHeaderLabels(["", "Name", "Group name", "Use guide", "Bake UV", "Animation", ""]) 1028 | self.table.horizontalHeader().setSectionResizeMode(0, QtWidgets.QHeaderView.Fixed) 1029 | self.table.setColumnWidth(0, 40) 1030 | self.table.setColumnWidth(3, 140) 1031 | self.table.horizontalHeader().setSectionResizeMode(3, QtWidgets.QHeaderView.Fixed) 1032 | self.table.setColumnWidth(4, 140) 1033 | self.table.horizontalHeader().setSectionResizeMode(4, QtWidgets.QHeaderView.Fixed) 1034 | self.table.setColumnWidth(5, 140) 1035 | self.table.horizontalHeader().setSectionResizeMode(5, QtWidgets.QHeaderView.Fixed) 1036 | self.table.setColumnWidth(6, 140) 1037 | self.table.horizontalHeader().setSectionResizeMode(6, QtWidgets.QHeaderView.Fixed) 1038 | self.table.horizontalHeader().setStretchLastSection(True) 1039 | self.table.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) # Multi Selection 1040 | self.table.setSelectionBehavior(QtWidgets.QTableView.SelectRows) 1041 | 1042 | self.table.setStyleSheet(""" 1043 | QTableView::item 1044 | { 1045 | border: 0px; 1046 | padding: 5px; 1047 | background-color: rgb(68, 68, 68); 1048 | } 1049 | QTableView::item:selected { 1050 | background-color: rgb(81, 133, 166); 1051 | } 1052 | QTableView::item QCheckBox { 1053 | padding-left:60px; 1054 | } 1055 | """) 1056 | 1057 | self.table.clearContents() 1058 | self.table.setRowCount(0) 1059 | 1060 | self.splitter = QtWidgets.QSplitter(QtCore.Qt.Horizontal) 1061 | 1062 | # self.table.cellClicked.connect(self.update_detail) 1063 | self.table.selectionModel().selectionChanged.connect(self.update_detail) 1064 | self.splitter.addWidget(self.table) 1065 | 1066 | # Detail view on the right 1067 | self.detail_widget = None 1068 | self.clear_detail() 1069 | # self.splitter.setSizes([1,0]) 1070 | 1071 | self.Bakeframe, frameLayout = self.createFrame(labelText="Bake UV") 1072 | 1073 | self.MeshName = QtWidgets.QLabel("Mesh : ---") 1074 | hBox = QtWidgets.QHBoxLayout() 1075 | hBox.setContentsMargins(10, 10, 10, 10) 1076 | hBox.addWidget(self.MeshName) 1077 | hBox2 = QtWidgets.QHBoxLayout() 1078 | label = QtWidgets.QLabel("UV Set : ") 1079 | hBox2.addWidget(label) 1080 | self.combo = QtWidgets.QComboBox() 1081 | self.combo.addItem(" --- ") 1082 | 1083 | self.uvSetStr = QtWidgets.QLabel("Selected: None") 1084 | 1085 | self.combo.currentIndexChanged.connect(self.update_uvset_label) 1086 | hBox2.addWidget(self.combo) 1087 | hBox.addStretch(2) 1088 | hBox.addLayout(hBox2) 1089 | hBox.addStretch(1) 1090 | 1091 | frameLayout.addLayout(hBox) 1092 | 1093 | self.button3 = QtWidgets.QPushButton("Pick other mesh", self) 1094 | self.button3.clicked.connect(self.pick_mesh) 1095 | self.button3.setSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) 1096 | frameLayout.addWidget(self.button3) 1097 | 1098 | self.separator = QtWidgets.QFrame(self) 1099 | self.separator.setFrameShape(QtWidgets.QFrame.HLine) 1100 | self.separator.setFrameShadow(QtWidgets.QFrame.Sunken) 1101 | 1102 | self.AnimationFrame, frameLayout = self.createFrame(labelText="Animation") 1103 | 1104 | validator = QtGui.QIntValidator() 1105 | validator.setRange(0, 99999) 1106 | self.startFrame = QtWidgets.QLineEdit() 1107 | self.startFrame.setMaximumWidth(60) 1108 | self.startFrame.setValidator(validator) 1109 | self.startFrame.setText(str(0)) 1110 | self.endFrame = QtWidgets.QLineEdit() 1111 | self.endFrame.setMaximumWidth(60) 1112 | self.endFrame.setValidator(validator) 1113 | self.endFrame.setText(str(0)) 1114 | self.preroll = QtWidgets.QCheckBox("Preroll") 1115 | 1116 | frameLayout.setContentsMargins(10, 10, 10, 10) 1117 | hBox = QtWidgets.QHBoxLayout() 1118 | hBox.addWidget(QtWidgets.QLabel("Frame Range : ")) 1119 | hBox.addWidget(self.startFrame) 1120 | hBox.addWidget(QtWidgets.QLabel(" ~ ")) 1121 | hBox.addWidget(self.endFrame) 1122 | hBox.addStretch(1) 1123 | hBox2 = QtWidgets.QHBoxLayout() 1124 | hBox2.addWidget(self.preroll) 1125 | hBox2.addStretch(1) 1126 | 1127 | frameLayout.addLayout(hBox) 1128 | frameLayout.addLayout(hBox2) 1129 | 1130 | self.SettingFrame, frameLayout = self.createFrame(labelText="Setting") 1131 | 1132 | frameLayout.setContentsMargins(10, 10, 10, 10) 1133 | hBox = QtWidgets.QHBoxLayout() 1134 | self.createGroupId_cb = QtWidgets.QCheckBox("Create group id") 1135 | self.createGroupId_cb.setChecked(True) 1136 | hBox.addWidget(self.createGroupId_cb) 1137 | 1138 | self.createCardId_cb = QtWidgets.QCheckBox("Create card id same as group name") 1139 | self.createCardId_cb.setChecked(False) 1140 | hBox.addWidget(self.createCardId_cb) 1141 | 1142 | frameLayout.addLayout(hBox) 1143 | 1144 | self.save_button = QtWidgets.QPushButton("Save Alembic File", self) 1145 | self.save_button.clicked.connect(self.save_abc) 1146 | self.clear_temp_button = QtWidgets.QPushButton("Clear Temp Data", self) 1147 | self.clear_temp_button.clicked.connect(self.clear_temp) 1148 | self.cancel_button = QtWidgets.QPushButton("Close", self) 1149 | self.cancel_button.clicked.connect(self.close) 1150 | 1151 | button_layout = QtWidgets.QHBoxLayout() 1152 | button_layout.addWidget(self.save_button) 1153 | button_layout.addWidget(self.clear_temp_button) 1154 | button_layout.addWidget(self.cancel_button) 1155 | 1156 | main_layout.addWidget(self.splitter) 1157 | main_layout.addWidget(self.Bakeframe) 1158 | main_layout.addWidget(self.AnimationFrame) 1159 | main_layout.addWidget(self.SettingFrame) 1160 | main_layout.addWidget(self.separator) 1161 | main_layout.addLayout(button_layout) 1162 | 1163 | self.setLayout(main_layout) 1164 | 1165 | def clear_detail(self): 1166 | old_sizes = None 1167 | if self.detail_widget is not None: 1168 | old_sizes = self.splitter.sizes() 1169 | self.detail_widget.setParent(None) 1170 | self.detail_label = QtWidgets.QLabel("Select an item to view details") 1171 | self.detail_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft) 1172 | self.detail_widget = QtWidgets.QWidget() 1173 | self.detail_layout = QtWidgets.QVBoxLayout() 1174 | self.detail_layout.addWidget(self.detail_label) 1175 | self.detail_widget.setLayout(self.detail_layout) 1176 | self.splitter.addWidget(self.detail_widget) 1177 | # self.detail_widget.setMaximumWidth(1000) 1178 | if old_sizes is None: 1179 | self.splitter.setStretchFactor(0, 4) 1180 | self.splitter.setStretchFactor(1, 1) 1181 | else: 1182 | self.splitter.setSizes(old_sizes) 1183 | 1184 | def create_detail_checkBox(self, prop): 1185 | selected_rows = [index.row() for index in self.table.selectionModel().selectedRows()] 1186 | if len(selected_rows) == 0: 1187 | return None 1188 | checkBox = QtWidgets.QCheckBox(self) 1189 | isTrue = getattr(self.contentList[selected_rows[0]], prop) 1190 | checkBox.setChecked(isTrue) 1191 | for row in selected_rows: 1192 | content = self.contentList[row] 1193 | if getattr(content, prop) != isTrue: 1194 | checkBox.setCheckState(QtCore.Qt.CheckState.PartiallyChecked) 1195 | break 1196 | 1197 | def onStateChange(state): 1198 | for row in selected_rows: 1199 | content = self.contentList[row] 1200 | setattr(content, prop, state == 2) 1201 | 1202 | checkBox.stateChanged.connect(onStateChange) 1203 | return checkBox 1204 | 1205 | def update_detail(self, indices): 1206 | self.clear_detail() 1207 | selected_rows = [index.row() for index in self.table.selectionModel().selectedRows()] 1208 | if len(selected_rows) == 0: 1209 | return 1210 | content = self.contentList[selected_rows[0]] 1211 | is_multi_selected = len(self.table.selectionModel().selectedRows()) > 1 1212 | vBox = QtWidgets.QVBoxLayout() 1213 | self.detail_layout.addLayout(vBox) 1214 | vBox2 = QtWidgets.QVBoxLayout() 1215 | self.detail_layout.addLayout(vBox2) 1216 | vBox2.addStretch(1) 1217 | 1218 | self.detail_label.setText(content.showName if not is_multi_selected else "--") 1219 | self.detail_label.setStyleSheet('font-weight:bold;margin-bottom:20px') 1220 | self.detail_label.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignCenter) # 设置对齐方式 1221 | 1222 | write_spline_animation = self.create_detail_checkBox('splineAnimation') 1223 | write_spline_animation.setText("write spline animation") 1224 | write_spline_animation.setToolTip( 1225 | "If writes animation, also write spline animation, not just the animation of guides.") 1226 | 1227 | vBox.addWidget(write_spline_animation) 1228 | 1229 | write_guide_id_cb = self.create_detail_checkBox('writePtexGuideId') 1230 | write_guide_id_cb.setText("write guide id from ptex") 1231 | write_guide_id_cb.setToolTip( 1232 | "Experimental feature, only supports versions up to UE5.3, writes properties such as groom_closest_guides.") 1233 | 1234 | # vBox.setContentsMargins(0,0,10,10) 1235 | vBox.addWidget(write_guide_id_cb) 1236 | 1237 | if is_multi_selected: 1238 | group_name_multi_edit = QtWidgets.QLineEdit() 1239 | 1240 | names_are_same = True 1241 | group_name = content.groupName.text() 1242 | for selected_row in selected_rows[1:]: 1243 | if self.contentList[selected_row].groupName.text() != group_name: 1244 | names_are_same = False 1245 | break 1246 | if names_are_same: 1247 | group_name_multi_edit.setText(group_name) 1248 | else: 1249 | group_name_multi_edit.setPlaceholderText(" -- ") 1250 | 1251 | def change_group_name(text): 1252 | for selected_row in selected_rows: 1253 | self.contentList[selected_row].groupName.setText(text) 1254 | 1255 | group_name_multi_edit.textEdited.connect(change_group_name) 1256 | hBox = QtWidgets.QHBoxLayout() 1257 | group_name_label = QtWidgets.QLabel("Group name: ") 1258 | hBox.addWidget(group_name_label) 1259 | hBox.addWidget(group_name_multi_edit) 1260 | vBox.addLayout(hBox) 1261 | 1262 | if not is_multi_selected: 1263 | label = QtWidgets.QLabel('Select .ptx file:') 1264 | # label.setStyleSheet('font-size:16px') 1265 | vBox.addWidget(label) 1266 | 1267 | def setPath(text): 1268 | content.regionPtex = text 1269 | 1270 | RegionPtex = FileSelectorWidget(setPath) 1271 | RegionPtex.set_file_path(content.regionPtex) 1272 | vBox.addWidget(RegionPtex) 1273 | 1274 | self.splitter.addWidget(self.detail_widget) 1275 | 1276 | def pick_mesh(self): 1277 | selectionList = om.MGlobal.getActiveSelectionList() 1278 | if selectionList.length() > 0: 1279 | dag_path = selectionList.getDagPath(0) 1280 | fnDepNode = om.MFnDependencyNode(dag_path.node()) 1281 | itDag = om.MItDag() 1282 | # find mesh 1283 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kMesh) 1284 | while not itDag.isDone(): 1285 | meshPath = om.MDagPath.getAPathTo(itDag.currentItem()) 1286 | mesh = om.MFnMesh(meshPath) 1287 | self.setBakeMesh(mesh) 1288 | break 1289 | 1290 | def update_uvset_label(self): 1291 | selected_option = self.combo.currentText() 1292 | self.uvSetStr.setText(selected_option) 1293 | 1294 | def clear_temp(self): 1295 | deleteSaveXGenDesWindowParent() 1296 | 1297 | def save_abc(self): 1298 | if len(self.contentList) == 0: 1299 | print("No content") 1300 | return 1301 | file_path = cmds.fileDialog2( 1302 | caption="Save Alembic File", 1303 | fileMode=0, 1304 | okCaption="save", 1305 | startingDirectory=self.save_path, 1306 | ff='Alembic Files (*.abc);;All Files (*)' 1307 | ) 1308 | if file_path: 1309 | self.save_path = file_path[0] 1310 | else: 1311 | return 1312 | selectionList = om.MGlobal.getActiveSelectionList() 1313 | startTime = time.time() 1314 | oldCurTime = omAnim.MAnimControl.currentTime() 1315 | archive = abc.OArchive(file_path[0]) 1316 | 1317 | anyAnimation = False 1318 | for item in self.contentList: 1319 | if item.export.isChecked(): 1320 | hasAnimation = item.animation.isChecked() 1321 | if hasAnimation: 1322 | anyAnimation = True 1323 | 1324 | if anyAnimation: 1325 | frameRange = [int(self.startFrame.text()), int(self.endFrame.text())] 1326 | if (frameRange[0] > frameRange[1] 1327 | or frameRange[0] < omAnim.MAnimControl.minTime().value 1328 | or frameRange[1] > omAnim.MAnimControl.maxTime().value): 1329 | raise ValueError("Frame out of range.") 1330 | # frameRange[0] = int(max(frameRange[0], omAnim.MAnimControl.minTime().value)) 1331 | # frameRange[1] = int(min(frameRange[1], omAnim.MAnimControl.maxTime().value)) 1332 | 1333 | sec = om.MTime(1, om.MTime.kSeconds) 1334 | spf = 1.0 / sec.asUnits(om.MTime.uiUnit()) 1335 | timeSampling = abcA.TimeSampling(spf, spf * frameRange[0]) 1336 | 1337 | timeIndex = archive.addTimeSampling(timeSampling) 1338 | proxyList = [] # All Alembic content should be destroyed at the end of the method, otherwise it will not be written to the file 1339 | setGroomGuideIdStartIndex(0) 1340 | for item in self.contentList: 1341 | if item.export.isChecked(): 1342 | fnDepNode = item.fnDepNode 1343 | needBakeUV = item.bakeUV.isChecked() 1344 | hasAnimation = item.animation.isChecked() 1345 | useGuide = item.useGuide.isChecked() 1346 | if hasAnimation: 1347 | curveObj = abcGeom.OCurves(archive.getTop(), fnDepNode.name(), timeIndex) 1348 | else: 1349 | curveObj = abcGeom.OCurves(archive.getTop(), fnDepNode.name()) 1350 | xgenProxy = XGenProxyEveryFrame(curveObj, item.fnDepNode, needBakeUV | useGuide, item.splineAnimation) 1351 | xgenProxy.needBakeUV = needBakeUV 1352 | xgenProxy.write_group_name(item.groupName.text(), self.createCardId_cb.isChecked()) 1353 | if useGuide: 1354 | # guides = GuidesToCurves(item.fnDepNode) 1355 | guideName = fnDepNode.name() + "_guide" 1356 | if hasAnimation: 1357 | curveObj = abcGeom.OCurves(archive.getTop(), guideName, timeIndex) 1358 | else: 1359 | curveObj = abcGeom.OCurves(archive.getTop(), guideName) 1360 | guideProxy = GuideProxy(curveObj, fnDepNode, False, hasAnimation) 1361 | guideProxy.write_group_name(item.groupName.text(), self.createCardId_cb.isChecked()) 1362 | guideProxy.write_is_guide(True) 1363 | proxyList.append(guideProxy) 1364 | guideProxy.set_xgen_proxy_and_ptex(xgenProxy, item.regionPtex) 1365 | guideProxy.writePtexGuideId = item.writePtexGuideId 1366 | proxyList.append(xgenProxy) # after guides, for baking 1367 | # return 1368 | if len(proxyList) == 0: 1369 | print("No content") 1370 | om.MGlobal.setActiveSelectionList(selectionList) 1371 | return 1372 | 1373 | if self.createGroupId_cb.isChecked(): 1374 | groupIds = dict() 1375 | currentId = 0 1376 | for proxy in proxyList: 1377 | if proxy.groupName not in groupIds: 1378 | groupIds[proxy.groupName] = currentId 1379 | currentId += 1 1380 | proxy.write_group_id(groupIds[proxy.groupName]) 1381 | 1382 | if anyAnimation: 1383 | if self.preroll.isChecked(): 1384 | for frame in range(int(omAnim.MAnimControl.minTime().value), frameRange[0]): 1385 | om.MGlobal.viewFrame(frame) 1386 | for frame in range(frameRange[0], frameRange[1] + 1): 1387 | om.MGlobal.viewFrame(frame) 1388 | for item in proxyList: 1389 | if frame == frameRange[0]: 1390 | item.write_first_frame() 1391 | elif item.animation: 1392 | item.write_frame() 1393 | omAnim.MAnimControl.setCurrentTime(oldCurTime) 1394 | else: 1395 | for item in proxyList: 1396 | item.write_first_frame() 1397 | for item in proxyList: 1398 | item.bake_uv(self.bakeMesh, self.uvSetStr.text()) 1399 | if isinstance(item, GuideProxy): 1400 | item.write_guide_id_from_ptex() 1401 | print("Data has been saved in %s, it took %.2f seconds." % (file_path[0], time.time() - startTime)) 1402 | om.MGlobal.setActiveSelectionList(selectionList) 1403 | return file_path[0] 1404 | 1405 | def fillTableWithSelectList(self): 1406 | self.clear_detail() 1407 | self.contentList = [] 1408 | selectionList = om.MGlobal.getActiveSelectionList() 1409 | contentList = [] 1410 | for i in range(selectionList.length()): 1411 | dag_path = selectionList.getDagPath(i) 1412 | fnDepNode = om.MFnDependencyNode(dag_path.node()) 1413 | if fnDepNode.typeName == 'xgmPalette': 1414 | continue 1415 | itDag = om.MItDag() 1416 | # find xgen description 1417 | itDag.reset(fnDepNode.object(), om.MItDag.kDepthFirst, om.MFn.kNamedObject) 1418 | xgDes = None 1419 | while not itDag.isDone(): 1420 | dn = om.MFnDependencyNode(itDag.currentItem()) 1421 | if dn.typeName == 'xgmDescription': 1422 | xgDes = dn 1423 | break 1424 | itDag.next() 1425 | if xgDes is not None: 1426 | content = SaveXGenDesWindow.Content(xgDes, fnDepNode.name(), fnDepNode.name(), True, 1427 | False, False, True) 1428 | content.regionPtex = getClumpingPtexPath(fnDepNode) 1429 | contentList.append(content) 1430 | boundMesh = self.findBoundMesh(xgDes) 1431 | if boundMesh is not None: 1432 | self.setBakeMesh(boundMesh) 1433 | 1434 | self.table.setRowCount(len(contentList)) 1435 | for row in range(len(contentList)): 1436 | self.table.setCellWidget(row, 0, contentList[row].export) 1437 | contentList[row].export.setStyleSheet("padding-left:8px") 1438 | item = QtWidgets.QTableWidgetItem(contentList[row].showName) 1439 | item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable) 1440 | self.table.setItem(row, 1, item) 1441 | 1442 | self.table.setCellWidget(row, 2, contentList[row].groupName) 1443 | self.table.setCellWidget(row, 3, contentList[row].useGuide) 1444 | self.table.setCellWidget(row, 4, contentList[row].bakeUV) 1445 | self.table.setCellWidget(row, 5, contentList[row].animation) 1446 | 1447 | self.contentList = contentList 1448 | 1449 | def findBoundMesh(self, xgDes): 1450 | itDg = om.MItDag() 1451 | itDg.reset(om.MFnDagNode(xgDes.object()).parent(0), om.MItDag.kDepthFirst, om.MFn.kPluginShape) 1452 | boundMesh = None 1453 | while not itDg.isDone(): 1454 | dn = om.MFnDependencyNode(itDg.currentItem()) 1455 | if dn.typeName == 'xgmSubdPatch': 1456 | boundMeshPlug: om.MPlug = dn.findPlug('geometry', False) 1457 | boundMesh = om.MFnMesh( 1458 | om.MDagPath.getAPathTo(boundMeshPlug.source().node())) 1459 | break 1460 | itDg.next() 1461 | return boundMesh 1462 | 1463 | def setBakeMesh(self, mesh: om.MFnMesh): 1464 | if mesh is not None: 1465 | self.bakeMesh = mesh 1466 | self.MeshName.setText(f"Mesh: {mesh.name()}") 1467 | self.combo.clear() 1468 | self.combo.addItems(mesh.getUVSetNames()) 1469 | 1470 | 1471 | SaveXGenDesWindowInstanceName = '_SaveXGenDesWindowInstance' 1472 | if SaveXGenDesWindowInstanceName not in globals(): 1473 | globals()[SaveXGenDesWindowInstanceName] = SaveXGenDesWindow() 1474 | globals()[SaveXGenDesWindowInstanceName].show() 1475 | # SaveXGenDesWindow().show() 1476 | --------------------------------------------------------------------------------