Hi,
I come back again asking for help/advice, for context we are making a game for a group project at school and we would like that game to include some destroyable buildings, since I’m aiming to be a technical artist it fell on me to find a good way to destroy the buildings made by the 3D artists, so I started developing a tool to easily shatter or cut a mesh, it is the biggest program I made by myself so far so I am sure it is very inefficient and hard to read but it works and has a good enough interface using QT so I am pretty happy, here is the code:
from asyncio.windows_events import NULL
import logging
import maya.cmds as cmds # type: ignore
from maya import OpenMayaUI as omui # type: ignore
import random
Version = cmds.about(version=True)
logging.basicConfig()
logger = logging.getLogger('Shatter Tool')
logger.setLevel(logging.DEBUG)
if int(Version) < 2025:
import Qt
from Qt import QtWidgets, QtCore, QtGui
if Qt.__binding__ == 'Pyside':
logger.debug('Using Pyside with shiboken')
from shiboken import wrapInstance # type: ignore
from Qt.QtCore import Signal # type: ignore
elif Qt.__binding__.startswith('PyQt'):
logger.debug('Using PyQt with Sip')
from sip import wrapinstance as wrapInstance # type: ignore
from Qt.QtCore import pyqtSignal as Signal # type: ignore
else:
logger.debug('Using Pyside2 with shiboken2')
from shiboken2 import wrapInstance # type: ignore
from Qt.QtCore import Signal # type: ignore
else:
logger.debug('Using Pyside6 with shiboken6')
from PySide6.QtCore import Qt # type: ignore
from PySide6 import QtCore, QtWidgets, QtGui # type: ignore
from shiboken6 import wrapInstance # type: ignore
from PySide6.QtCore import Signal # type: ignore
def getMayaMainWindow():
if int(Version) < 2025:
win = omui.MQtUtil_mainWindow()
ptr = wrapInstance(int(win), QtWidgets.QMainWindow)
return ptr
else:
win = omui.MQtUtil.mainWindow()
ptr = wrapInstance(int(win), QtWidgets.QMainWindow)
return ptr
class ShatterUI (QtWidgets.QWidget):
"""
Basic UI that creates a shape to Cut Mesh a certain way
"""
def __init__ (self):
parent = QtWidgets.QDialog(parent = getMayaMainWindow())
parent.setObjectName ('Shatter Tool')
parent.setWindowTitle('Shatter Tool')
layout = QtWidgets.QVBoxLayout(parent)
super(ShatterUI, self).__init__(parent = parent)
self.BuildUI()
self.parent().layout().addWidget(self)
parent.show()
def BuildUI(self):
layout = QtWidgets.QGridLayout(self)
self.HistoryBtn = QtWidgets.QPushButton("Delete History")
self.HistoryBtn.clicked.connect(self.DeleteHistory)
layout.addWidget(self.HistoryBtn, 0, 1)
self.DuplicateBtn = QtWidgets.QPushButton("Duplicate Object")
self.DuplicateBtn.clicked.connect(self.Duplicate)
layout.addWidget(self.DuplicateBtn, 0, 2)
self.CenterBtn = QtWidgets.QPushButton("Center + Freeze")
self.CenterBtn.clicked.connect(self.MoveToCenter)
layout.addWidget(self.CenterBtn, 0, 3)
self.AddPlaneBtn = QtWidgets.QPushButton("Add Plane")
self.AddPlaneBtn.clicked.connect(self.AddPlane)
self.AddPlaneBtn.setDisabled(True)
layout.addWidget(self.AddPlaneBtn,2,2)
self.AddSphereBtn = QtWidgets.QPushButton("Add Sphere")
self.AddSphereBtn.clicked.connect(self.AddSphere)
self.AddSphereBtn.setDisabled(True)
layout.addWidget(self.AddSphereBtn,2,1)
self.ToggleXRBTN = QtWidgets.QPushButton("Toggle XRay")
self.ToggleXRBTN.clicked.connect(self.ToggleXR)
layout.addWidget(self.ToggleXRBTN,2,3)
self.DeformSlider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.DeformLabel = QtWidgets.QLabel("Deform Value 0")
self.DeformSlider.setSingleStep(2)
self.DeformSlider.setMinimum(0)
self.DeformSlider.setMaximum(5)
layout.addWidget(self.DeformLabel,3,1)
layout.addWidget(self.DeformSlider,3,2,1,2)
self.DeformSlider.valueChanged.connect(self.update_Deform)
self.SubSlider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.SubLabel = QtWidgets.QLabel("Subdiv Value 1")
self.SubSlider.setMinimum(1)
self.SubSlider.setMaximum(10)
layout.addWidget(self.SubLabel,4,1)
layout.addWidget(self.SubSlider,4,2,1,2)
self.SubSlider.valueChanged.connect(self.update_Sub)
self.StartStopBtn = QtWidgets.QPushButton("START")
self.StartStopBtn.setCheckable(True)
self.StartStopBtn.clicked.connect(self.toggle)
self.StartStopBtn.setStyleSheet("background-color : green")
layout.addWidget(self.StartStopBtn,1,1,1,3)
self.CutBtn = QtWidgets.QPushButton("CUT")
self.CutBtn.clicked.connect(self.cut)
self.CutBtn.setStyleSheet("QPushButton:Disabled {background-color: darkslategrey; color: grey}")
self.CutBtn.setDisabled(True)
layout.addWidget(self.CutBtn,5,1)
self.CopyBtn = QtWidgets.QPushButton("Copy")
self.CopyBtn.clicked.connect(self.Copy)
self.CopyBtn.setDisabled(True)
layout.addWidget(self.CopyBtn,5,2)
self.DeleteBtn = QtWidgets.QPushButton("Delete")
self.DeleteBtn.clicked.connect(self.Delete)
self.DeleteBtn.setDisabled(True)
layout.addWidget(self.DeleteBtn,5,3)
"""Updating the Sliders"""
def update_Sub(self):
value = self.SubSlider.value()
self.SubLabel.setText(f"Subdiv Value: {value}")
self.Subdivise(value)
def update_Deform(self):
value = self.DeformSlider.value()
self.DeformLabel.setText(f"Deform Value: {value}")
self.Deform(value)
def toggle(self):
if self.StartStopBtn.isChecked():
self.ToShatter = self.GetMesh()
self.StartStopBtn.setText("STOP")
self.StartStopBtn.setStyleSheet("QPushButton:checked {background-color: red; border: red; min-height: 1.8em;}")
self.CenterBtn.setDisabled(True)
self.DuplicateBtn.setDisabled(True)
self.HistoryBtn.setDisabled(True)
self.AddSphereBtn.setDisabled(False)
self.AddPlaneBtn.setDisabled(False)
self.DeleteBtn.setDisabled(False)
self.CutBtn.setDisabled(False)
self.CutBtn.setStyleSheet("background-color: darkturquoise;")
self.CopyBtn.setDisabled(False)
self.CutMesh = {}
#cmds.createDisplayLayer(name = "Shatter")
else:
self.StartStopBtn.setText("Start")
self.StartStopBtn.setStyleSheet("background-color : green")
self.CenterBtn.setDisabled(False)
self.DuplicateBtn.setDisabled(False)
self.HistoryBtn.setDisabled(False)
self.AddSphereBtn.setDisabled(True)
self.AddPlaneBtn.setDisabled(True)
self.DeleteBtn.setDisabled(True)
self.CutBtn.setStyleSheet("QPushButton:Disabled {background-color: darkslategrey; color: grey}")
self.CutBtn.setDisabled(True)
self.CopyBtn.setDisabled(True)
cmds.polySeparate( self.ToShatter )
def Copy(self):
selected = self.GetMesh()
NewlyCopied = []
for item in selected:
Poly = self.CutMesh[item]
translate_x = cmds.getAttr("%s.translateX" % item)
translate_y = cmds.getAttr("%s.translateY" % item)
translate_z = cmds.getAttr("%s.translateZ" % item)
scale_x = cmds.getAttr("%s.scaleX" % item)
scale_y = cmds.getAttr("%s.scaleY" % item)
scale_z = cmds.getAttr("%s.scaleZ" % item)
rotate_x = cmds.getAttr("%s.rotateX" % item)
rotate_y = cmds.getAttr("%s.rotateY" % item)
rotate_z = cmds.getAttr("%s.rotateZ" % item)
if "Plane" in item:
SubdiW = cmds.getAttr("%s.subdivisionsWidth" % Poly)
#SubdiD = cmds.getAttr("%s.subdivisionsDepth" % Poly)
SubdiH = cmds.getAttr("%s.subdivisionsHeight" % Poly)
Height = cmds.getAttr("%s.height" % Poly)
newCube = cmds.polyPlane()
cmds.setAttr(newCube[0] + ".translateX", translate_x)
cmds.setAttr(newCube[0] + ".translateY", translate_y)
cmds.setAttr(newCube[0] + ".translateZ", translate_z)
cmds.setAttr(newCube[0] + ".scaleX", scale_x)
cmds.setAttr(newCube[0] + ".scaleY", scale_y)
cmds.setAttr(newCube[0] + ".scaleZ", scale_z)
cmds.setAttr(newCube[0] + ".rotateX", rotate_x)
cmds.setAttr(newCube[0] + ".rotateY", rotate_y)
cmds.setAttr(newCube[0] + ".rotateZ", rotate_z)
cmds.setAttr(newCube[1] + ".subdivisionsWidth", SubdiW)
#cmds.setAttr(newCube[1] + ".subdivisionsDepth", SubdiD)
cmds.setAttr(newCube[1] + ".subdivisionsHeight", SubdiH)
cmds.setAttr(newCube[1] + ".height", Height)
self.CutMesh[newCube[0]] = newCube[1]
NewlyCopied.append(newCube[0])
elif "Sphere" in item:
SubdiH = cmds.getAttr("%s.subdivisionsHeight" % Poly)
SubdiA = cmds.getAttr("%s.subdivisionsAxis" % Poly)
newSphere = cmds.polySphere()
cmds.setAttr(newSphere[0] + ".translateX", translate_x)
cmds.setAttr(newSphere[0] + ".translateY", translate_y)
cmds.setAttr(newSphere[0] + ".translateZ", translate_z)
cmds.setAttr(newSphere[0] + ".scaleX", scale_x)
cmds.setAttr(newSphere[0] + ".scaleY", scale_y)
cmds.setAttr(newSphere[0] + ".scaleZ", scale_z)
cmds.setAttr(newSphere[0] + ".rotateX", rotate_x)
cmds.setAttr(newSphere[0] + ".rotateY", rotate_y)
cmds.setAttr(newSphere[0] + ".rotateZ", rotate_z)
cmds.setAttr(newSphere[1] + ".subdivisionsHeight", SubdiH)
cmds.setAttr(newSphere[1] + ".subdivisionsAxis", SubdiA)
self.CutMesh[newSphere[0]] = newSphere[1]
NewlyCopied.append(newSphere[0])
print (NewlyCopied)
cmds.select(NewlyCopied)
"""Deletes Selected Cutting Meshes"""
def Delete(self):
select = self.GetMesh()
count = 0
for items in select:
ToDelete = select[count]
if ToDelete in self.CutMesh:
self.CutMesh.pop(ToDelete)
print(ToDelete + " is Deleted")
count = count + 1
print (self.CutMesh)
cmds.delete()
def cut(self):
"""add subdivision to selected Cutting Mesh"""
sel = self.GetMesh()
ShatterList = []
for item in sel:
cmds.polyExtrudeFacet (item, tk = 0.001)
if item in self.CutMesh:
ShatterList.append(item)
CutMesh = cmds.polyCBoolOp( self.ToShatter, ShatterList, op=2, n=str(self.ToShatter))
self.ToShatter = CutMesh[0]
cmds.delete(self.ToShatter,constructionHistory = True)
def Deform(self,Deformation):
#Deformation = Deformation * 0.1
sel = cmds.ls(sl=True, o=True)
for item in sel:
NumVer = cmds.polyEvaluate( v=True)
Vertex = 0
itemSTR = self.CutMesh[item]
if "Plane" in item:
while Vertex < NumVer:
cmds.setAttr ("pPlaneShape" + str(int(''.join(filter(str.isdigit, itemSTR)))) + ".pnts[" + str(Vertex) + "]" + ".pnty", random.randint(0, Deformation)*0.02)
Vertex = Vertex + 2
elif "Sphere" in item:
while Vertex < NumVer:
cmds.setAttr ("pSphereShape" + str(int(''.join(filter(str.isdigit, itemSTR)))) + ".pnts[" + str(Vertex) + "]" + ".pnty", random.randint(0, Deformation)*0.03)
cmds.setAttr ("pSphereShape" + str(int(''.join(filter(str.isdigit, itemSTR)))) + ".pnts[" + str(Vertex) + "]" + ".pntx", random.randint(0, Deformation)*0.03)
Vertex = Vertex + 2
def Subdivise(self,Subdivisions):
selected = self.GetMesh()
for item in selected:
Poly = self.CutMesh[item]
if 'Plane' in item:
cmds.setAttr( Poly + ".subdivisionsWidth", Subdivisions)
cmds.setAttr( Poly + ".subdivisionsHeight", Subdivisions)
else:
if Subdivisions < 4:
return
cmds.setAttr( Poly + ".subdivisionsHeight", Subdivisions)
"""Add a plane to cut into the mesh"""
def AddPlane(self):
plane = cmds.polyPlane( w=1,sx=1, sy=1, h=1)
self.CutMesh[plane[0]] = plane[1]
def AddSphere(self):
sphere = cmds.polySphere( sx=10, sy=10, r=1)
self.CutMesh[sphere[0]] = sphere[1]
"""Toggles XR View ON or OFF"""
def ToggleXR(self):
Panel = ["1", "2", "3", "4"]
length = len(Panel)
for obj in range(length):
result = cmds.modelEditor('modelPanel' + Panel[obj], q=True, xr=True,wireframeOnShaded = True)
cmds.modelEditor('modelPanel' + Panel[obj], e=True, xr=not result, wireframeOnShaded =not result)
"""Moves Selected Mesh to World Center"""
def MoveToCenter(self):
ToMove = self.GetMesh()
cmds.move (0, 0, 0, ToMove, rpr=True, a=True)
cmds.makeIdentity (ToMove, a = True, t = True, r = True,s = True, n = False, pn =True)
"""Duplicates Selected Mesh and adds 'Shatter' to the name"""
def Duplicate(self):
ToDuplicate = self.GetMesh()
cmds.duplicate (ToDuplicate)
cmds.hide (ToDuplicate)
ToRename = self.GetMesh()
NewName = ToRename[0] + "_" + "shatter"
cmds.rename(ToRename, NewName)
"""Deletes History"""
def DeleteHistory(self):
toDelete = self.GetMesh()
cmds.delete(toDelete,constructionHistory = True)
print ("History Deleted")
def GetMesh(self):
Selected = cmds.ls(selection = True)
if not Selected:
raise RuntimeError ("Nothing is selected")
return Selected
here are some pictures:
So the code itself works but since I am using Booleans it has some defaults:
it works only on lowpoly; or Booleans dont work/crash/bug
you cannot use an infinite “cut” at some points it bugs out
it’s still probable we’ll use this in case I cannot find something else, but looking online I found this explanation on Voronoi shatter tool:
So I thought I could give it a try, but unfortunately, it doesn’t seem to work. I added some QT interface and I thought it might be why it doesn’t work but even taking the website’s source code I have the same issue:
it uses a nested loop to cut into the shape, but the nested loop only works the first time, and I can’t figure out why?
here is the code:
#from asyncio.windows_events import NULL
import logging
import maya.cmds as cmds # type: ignore
from maya import OpenMayaUI as omui # type: ignore
import random
Version = cmds.about(version=True)
logging.basicConfig()
logger = logging.getLogger('Voronoi Shatter Tool')
logger.setLevel(logging.DEBUG)
if int(Version) < 2025:
import Qt
from Qt import QtWidgets, QtCore, QtGui
if Qt.__binding__ == 'Pyside':
logger.debug('Using Pyside with shiboken')
from shiboken import wrapInstance # type: ignore
from Qt.QtCore import Signal # type: ignore
elif Qt.__binding__.startswith('PyQt'):
logger.debug('Using PyQt with Sip')
from sip import wrapinstance as wrapInstance # type: ignore
from Qt.QtCore import pyqtSignal as Signal # type: ignore
else:
logger.debug('Using Pyside2 with shiboken2')
from shiboken2 import wrapInstance # type: ignore
from Qt.QtCore import Signal # type: ignore
else:
logger.debug('Using Pyside6 with shiboken6')
from PySide6.QtCore import Qt # type: ignore
from PySide6 import QtCore, QtWidgets, QtGui # type: ignore
from shiboken6 import wrapInstance # type: ignore
from PySide6.QtCore import Signal # type: ignore
def getMayaMainWindow():
if int(Version) < 2025:
win = omui.MQtUtil_mainWindow()
ptr = wrapInstance(int(win), QtWidgets.QMainWindow)
return ptr
else:
win = omui.MQtUtil.mainWindow()
ptr = wrapInstance(int(win), QtWidgets.QMainWindow)
return ptr
class VoronoiShatterUI (QtWidgets.QWidget):
"""
Basic UI that creates a shape to Cut Mesh a certain way
"""
def __init__ (self):
parent = QtWidgets.QDialog(parent = getMayaMainWindow())
parent.setObjectName ('Voronoi Shatter Tool')
parent.setWindowTitle('Voronoi Shatter Tool')
layout = QtWidgets.QVBoxLayout(parent)
super(VoronoiShatterUI, self).__init__(parent = parent)
self.BuildUI()
self.parent().layout().addWidget(self)
parent.show()
def BuildUI(self):
layout = QtWidgets.QGridLayout(self)
self.ChunksSlider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
self.ChunkLabel = QtWidgets.QLabel("Chunks Amount 1")
self.ChunksSlider.setMinimum(1)
self.ChunksSlider.setMaximum(10)
layout.addWidget(self.ChunkLabel,0,0)
layout.addWidget(self.ChunksSlider,0,1)
self.ChunksSlider.valueChanged.connect(self.update_chunks)
self.StartStopBtn = QtWidgets.QPushButton("START")
self.StartStopBtn.clicked.connect(self.Start)
self.StartStopBtn.setStyleSheet("background-color : green")
layout.addWidget(self.StartStopBtn,1,0,1,3)
def update_chunks(self):
value = self.ChunksSlider.value()
self.ChunkLabel.setText(f"Subdiv Value: {value}")
#self.Subdivise(value)
def Start(self):
#value = self.ChunksSlider.value()
ToCut = self.GetMesh()
BoundingBox = cmds.exactWorldBoundingBox(ToCut)
PointsCount = self.ChunksSlider.value()
VoroX = [random.uniform(BoundingBox[0], BoundingBox[3]) for i in range(PointsCount)]
VoroY = [random.uniform(BoundingBox[1], BoundingBox[4]) for i in range(PointsCount)]
VoroZ = [random.uniform(BoundingBox[2], BoundingBox[5]) for i in range(PointsCount)]
voroPoints = zip(VoroX,VoroY,VoroZ)
voroPoint = zip(VoroX,VoroY,VoroZ)
for ToCopies in voroPoints:
Copies = cmds.duplicate(ToCut)
print ("Duplicate is working")
for VoroCut in voroPoint:
print ("passed")
if ToCopies != VoroCut:
aim = [(vec1 - vec2) for (vec1, vec2) in zip(ToCopies, VoroCut)]
VoroCenter = [(vec1 + vec2)/2 for (vec1, vec2) in zip(VoroCut, ToCopies)]
planeAngle = cmds.angleBetween (euler = True, v1 = [0,0,1], v2 = aim)
cmds.polyCut(Copies[0], df = True, cutPlaneCenter = VoroCenter, cutPlaneRotate = planeAngle)
cmds.polyCloseBorder(Copies[0])
def GetMesh(self):
Selected = cmds.ls(selection = True)
if not Selected:
raise RuntimeError ("Nothing is selected")
return Selected
So to summarize:
The first tool I made works but relies on booleans which seem to be untrustworthy, is there any way to reduce the bugs with them, or change the code to make it more stable?
O̶n̶ t̶h̶e̶ s̶e̶c̶o̶n̶d̶ c̶o̶d̶e̶ I̶ f̶o̶u̶n̶d̶, I̶f̶ a̶n̶y̶o̶n̶e̶ c̶a̶n̶ h̶e̶l̶p̶ m̶e̶ f̶i̶x̶ t̶h̶e̶ i̶s̶s̶u̶e̶ w̶i̶t̶h̶ t̶h̶e̶ n̶e̶s̶t̶e̶d̶ l̶o̶o̶p̶/t̶e̶l̶l̶ m̶e̶ w̶h̶y̶ i̶t̶ w̶o̶r̶k̶s̶ o̶n̶l̶y̶ o̶n̶c̶e̶ t̶h̶a̶t̶ w̶o̶u̶l̶d̶ b̶e̶ v̶e̶r̶y̶ h̶e̶l̶p̶f̶u̶l̶, i̶f̶ i̶t̶ s̶e̶e̶m̶s̶ t̶o̶ p̶e̶r̶f̶o̶r̶m̶ b̶e̶t̶t̶e̶r̶ t̶h̶a̶n̶ m̶y̶ c̶o̶d̶e̶ I̶ w̶i̶l̶l̶ t̶r̶y̶ t̶o̶ u̶s̶e̶ v̶o̶r̶o̶n̶o̶i̶ t̶o̶o̶
→ didn’t work because of the way I wrote it, it only allowed one iteration and then skipped, see this:
And lastly, any other way to achieve what I want? not including buying an already-made tool
the buildings that we want to be breakable will most likely be empty inside/low poly
Thank you for your help!
*The “Shatter” tool in maya’s FX doesn’t to work
**We can use Maya, Zbrush and Unity only, no other 3D programs like Hoodini etc