You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

691 lines
30 KiB
Python

import os, time, subprocess
from pymxs import runtime as rt
try: from PySide6 import QtWidgets, QtCore, QtGui
except ImportError: from PySide2 import QtWidgets, QtCore, QtGui
import vr4life_cloud as cld
def get_vdenoise_path():
for k, v in os.environ.items():
if "VRAY" in k.upper() and "MAX" in k.upper() and "MAIN" in k.upper():
p = os.path.join(v, "vdenoise.exe")
if os.path.exists(p): return f'"{p}"'
return "vdenoise.exe"
# Injeção segura na MAXScript Listener. O Python via PySide desvia stdout nativo,
# e chamar rt.execute() mil vezes destrói a memória (c0000005).
# Aqui criamos a função NO MAXSCRIPT APENAS UMA VEZ e a referenciamos.
rt.execute("""global vr4life_mprint_func
fn vr4life_mprint_func msg = (
format "[VR4Life] %\\n" msg
)
global vr4life_popup_killer
fn vr4life_popup_killer = (
local hwnd = DialogMonitorOPS.GetWindowHandle()
if (hwnd != 0) do (
local title = UIAccessor.GetWindowText hwnd
if (matchPattern title pattern:"*V-Ray*" ignoreCase:true) or (matchPattern title pattern:"*Corona*" ignoreCase:true) do (
UIAccessor.PressButtonByName hwnd "Proceed"
UIAccessor.PressButtonByName hwnd "Yes"
UIAccessor.PressButtonByName hwnd "OK"
UIAccessor.PressButtonByName hwnd "Continue"
)
)
true
)""")
import builtins
def mprint(*args, **kwargs):
try:
msg = " ".join([str(a) for a in args])
rt.vr4life_mprint_func(msg) # Changed to call vr4life_mprint_func
except: pass
builtins.print = mprint # Redireciona todos os prints antigos deste arquivo pra Aba MAXScript F11
def load_bake_elements(ui):
ui.cmb_bake_elem.clear()
found = ["CompleteMap", "VRayCompleteMap", "Corona_Beauty", "CShading_Beauty"]
try:
r_list = rt.execute("for c in bake_elements.classes collect (c as string)")
for c in r_list:
if str(c) not in found: found.append(str(c))
except: pass
ui.cmb_bake_elem.addItems(sorted(list(set(found))))
try:
rnd = str(rt.execute("renderers.current as string"))
t = "VRayCompleteMap" if "V_Ray" in rnd else "Corona_Beauty" if "Corona" in rnd else "CompleteMap"
idx = ui.cmb_bake_elem.findText(t)
if idx >= 0: ui.cmb_bake_elem.setCurrentIndex(idx)
except: pass
def load_selection(ui):
mprint("======== [VR4Life] GATILHO DA INTERFACE: P1 [Lista] ========")
mprint("Lendo viewport para resgatar geometrias selecionadas...")
if not rt.execute("selection as array"):
mprint("ERRO: O usuario clicou em P1 mas nao ha nada selecionado.")
QtWidgets.QMessageBox.warning(ui, "Aviso", "Selecione objetos na viewport primeiro!")
return
ms = """(
undo "Fix Geometry" on (
local sel = selection as array
local groupHeads = for o in sel where isGroupHead o collect o
for g in groupHeads do ( if isValidNode g then explodeGroup g )
max modify mode
local finalSel = selection as array
local validObjs = #()
for obj in finalSel do (
if (isValidNode obj) and (superclassof obj == GeometryClass) and (classof obj != TargetObject) and (isGroupHead obj == false) then (
if canConvertTo obj Editable_Poly then convertToPoly obj
appendIfUnique validObjs obj
)
)
validObjs
)
)"""
sel = rt.execute(ms)
if not sel:
QtWidgets.QMessageBox.warning(ui, "Aviso", "Nenhuma geometria selecionada pós-explode.")
return
existing_names = [d['name'] for d in ui.bake_items]
tl = []
for o in sel:
n = str(o.name)
if n not in existing_names:
poly_count = rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0
tl.append({'name': n, 'poly': poly_count})
if not tl:
QtWidgets.QMessageBox.information(ui, "Info", "Seleção já existe na lista.")
return
tl.sort(key=lambda x: x['poly'], reverse=True)
for d in tl:
i = QtWidgets.QTreeWidgetItem([d['name'], "Pronto", f"{d['poly']:,}", "", ""])
i.setFlags(i.flags() | QtCore.Qt.ItemIsUserCheckable)
i.setCheckState(0, QtCore.Qt.Checked)
ui.tree.addTopLevelItem(i)
ui.bake_items.append({'name': d['name'], 'item': i})
ui.pb.setFormat(f"Adicionados: {len(tl)} / Total: {len(ui.bake_items)}")
ui.pb.setValue(0)
ui.upd_res_col()
mprint(f"SUCESSO! {len(tl)} geometrias importadas para a tabela do UI com poligonos calculados.")
def attach_grouped_objects(ui, from_auto=False):
ui.pb.setFormat("Fundindo grupos inteiros...")
QtWidgets.QApplication.processEvents()
ms = """(
fn hasProtectedName nd = (
local nm = toLower nd.name
if (matchPattern nm pattern:"*_mult*") or (matchPattern nm pattern:"*_catg*") or (matchPattern nm pattern:"*_mat*") do return true
for c in nd.children do ( if hasProtectedName c do return true )
return false
)
local act = 0; local new_objs = #(); local orig_sel = selection as array;
local loose = for o in orig_sel where not isGroupHead o and not isGroupMember o collect o;
local heads = for o in orig_sel where isGroupHead o and not (hasProtectedName o) collect o
for h in heads do (
local valid_cg = for c in h.children where superclassof c == GeometryClass and classof c != TargetObject collect c
if valid_cg.count > 0 do (
local b = valid_cg[1]
if not isKindOf b Editable_Poly do try(convertToPoly b)catch()
if isKindOf b Editable_Poly do (
for i = 2 to valid_cg.count do (
local n = valid_cg[i]
if not isKindOf n Editable_Poly do try(convertToPoly n)catch()
if isKindOf n Editable_Poly do try(polyOp.attach b n)catch()
)
b.name = h.name
setGroupMember b false
append new_objs b
act += 1
)
)
try(delete h)catch()
)
local final_sel = loose; for no in new_objs do append final_sel no; if final_sel.count > 0 do select final_sel; act
)"""
try:
act = rt.execute(ms)
load_selection(ui)
if not from_auto:
if act > 0: QtWidgets.QMessageBox.information(ui, "Sucesso", f"{act} Grupos fundidos.")
else: QtWidgets.QMessageBox.information(ui, "Aviso", "Nenhum Grupo encontrado.")
except Exception as e: print("Erro Solda:", e)
ui.pb.setValue(0); ui.pb.setFormat("Pronto")
def super_attach_objects(ui, from_auto=False):
ui.pb.setFormat("Super Solda VR...")
QtWidgets.QApplication.processEvents()
ms = """(
fn hasProtectedName nd = (
local nm = toLower nd.name
if (matchPattern nm pattern:"*_mult*") or (matchPattern nm pattern:"*_catg*") or (matchPattern nm pattern:"*_mat*") do return true
for c in nd.children do ( if hasProtectedName c do return true )
return false
)
local sel = for o in selection where superclassof o == GeometryClass and classof o != TargetObject and not (hasProtectedName o) collect o
if sel.count > 1 do (
local b = sel[1]
if not isKindOf b Editable_Poly do try(convertToPoly b)catch()
if isKindOf b Editable_Poly do (
for i = 2 to sel.count do (
local n = sel[i]
if not isKindOf n Editable_Poly do try(convertToPoly n)catch()
if isKindOf n Editable_Poly do try(polyOp.attach b n)catch()
)
b.name = uniqueName "VR_Bloco_Fundido"
select b
)
)
sel.count
)"""
try:
act = rt.execute(ms)
load_selection(ui)
if not from_auto:
if act > 1: QtWidgets.QMessageBox.information(ui, "Sucesso", f"{act} objetos fundidos.")
else: QtWidgets.QMessageBox.information(ui, "Aviso", "Selecione pelo menos 2 objetos.")
except Exception as e: print("Erro Super Solda:", e)
ui.pb.setValue(0); ui.pb.setFormat("Pronto")
def optimize_geometry(ui):
mprint("======== [VR4Life] GATILHO DA INTERFACE: P3 [Optimize] ========")
sp = ui.spn_pct.value()
tg = ui.spn_min_poly.value()
tgs = ui.get_processable_items()
tot = len(tgs)
ui.pb.setMaximum(tot)
ui.pb.setValue(0)
rt.execute("max modify mode")
for i, d in enumerate(tgs):
if ui._is_cancelled: break
ui.pb.setFormat(f"Opt ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i)
QtWidgets.QApplication.processEvents()
o = rt.getNodeByName(d['name'])
if o:
try:
cp = rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0
if cp < 50:
d['item'].setText(1, "Geo Base")
else:
rt.select(o)
if not rt.isKindOf(o, rt.Editable_Poly):
rt.convertTo(o, rt.Editable_Poly)
opt = rt.ProOptimizer()
rt.addModifier(o, opt)
opt.KeepTextures = True
opt.KeepNormals = True
opt.KeepBorders = True
opt.LockMat = True
opt.Calculate = True
current_pct = 100.0
p = 0
while True:
if ui._is_cancelled: break
current_pct = current_pct * (sp / 100.0)
opt.VertexPercent = current_pct
rt.redrawViews()
QtWidgets.QApplication.processEvents()
np = rt.getPolygonCount(o)[0]
p += 1
d['item'].setText(2, f"{np:,}")
d['item'].setText(1, f"Opt ({p}x)")
QtWidgets.QApplication.processEvents()
if np <= tg or np < 50 or p >= 20 or (np >= cp and p > 1): break
cp = np
rt.collapseStack(o)
if not rt.isKindOf(o, rt.Editable_Poly):
rt.convertTo(o, rt.Editable_Poly)
if not ui._is_cancelled:
d['item'].setText(2, f"{rt.getPolygonCount(o)[0]:,}")
d['item'].setText(1, f"Opt OK ({p}x)")
except Exception as e:
d['item'].setText(1, "Erro Opt")
print(f"Erro Opt {d['name']}: {e}")
ui.pb.setValue(tot)
ui.pb.setFormat("Opt Concluída!")
def prepare_mesh_v19(ui):
mprint("======== [VR4Life] GATILHO DA INTERFACE: P5 [Prepare UVP5] ========")
tgs = ui.get_processable_items()
tot = len(tgs)
ui.pb.setMaximum(tot)
ui.pb.setValue(0)
for i, d in enumerate(tgs):
if ui._is_cancelled: break
ui.pb.setFormat(f"UV v19 ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i)
QtWidgets.QApplication.processEvents()
o = rt.getNodeByName(d['name'])
if o:
try:
ms = f"""(
local obj = getNodeByName "{d['name']}"
if obj != undefined do (
max modify mode
select obj
local theMod = Unwrap_UVW()
modPanel.addModToSelection theMod ui:on
-- Usando Channel 2 para o Bake (Corrigido o bug que gerava Channel 3 e deixava as UVs dessincronizadas do motor render)
theMod.setMapChannel 2
-- Lógica original do V19 reconstruída para "Pack Custom" real
theMod.flattenMap 45.0 #() 0.01 true 0 true true
theMod.setTVSubObjectMode 3
theMod.selectFaces #{{1..(theMod.numberPolygons())}}
-- Configurando o Menu "Arrange Elements" (Pack Custom) exatamente como no Print:
-- Usar Recursivo(1), Padding(0.001), Rescale=True(Normalize), Rotate=False, FillHoles=False
theMod.pack 1 0.001 true false false
-- Não damos collapseStack a pedido do usuário!
)
)"""
rt.execute(ms)
d['item'].setText(1, "UV V19 (C2)")
except Exception as e:
d['item'].setText(1, "Erro UV")
print(f"Erro UV V19 em {d['name']}: {e}")
ui.pb.setValue(tot)
ui.pb.setFormat("UV Pack V19 Concluído!")
def close_annoying_windows():
ms = """(
try (
local desktopHWND = windows.getDesktopHWND()
local children = windows.getChildrenHWND desktopHWND
local targets = #("Corona", "V-Ray", "Buffer", "Render", "Warning", "Error")
for output in children do (
local hwnd = output[1]
local title = output[5]
for t in targets do (
if (matchPattern title pattern:("*" + t + "*") ignoreCase:true) then (
try ( UIAccessor.CloseDialog hwnd ) catch ()
)
)
)
) catch ()
)"""
rt.execute(ms)
def inspect_uv(ui):
tgs = ui.get_processable_items()
if len(tgs) != 1:
QtWidgets.QMessageBox.warning(ui, "Aviso", "Selecione exatamente UM objeto na lista para auditar a UV.")
return
obj_name = tgs[0]['name']
ms = f"""(
local o = getNodeByName "{obj_name}"
if o != undefined and isValidNode o do (
select o
max modify mode
if o.modifiers[#Unwrap_UVW] != undefined then (
modPanel.setCurrentObject o.modifiers[#Unwrap_UVW]
o.modifiers[#Unwrap_UVW].edit()
) else (
messageBox "O modificador Unwrap_UVW nao foi encontrado."
)
)
)"""
rt.execute(ms)
def process_bake_logic_v19(ui, auto_export=False):
print("\n" + "="*50)
print("🎬 INICIANDO LOG: MOTOR DE BAKE V19 PORTADO (P6)")
ui._is_cancelled = False
tgs = ui.get_processable_items()
if not tgs: return
tot = len(tgs)
ui.pb.setMaximum(tot)
ui.pb.setValue(0)
# Extract UI variables once
p_bk = ui.edt_p_bake.text().replace("\\", "/")
if not p_bk.endswith("/"): p_bk += "/"
if not os.path.exists(p_bk): os.makedirs(p_bk)
try: res_val = int(ui.spn_res.value())
except: res_val = 512
cv_passes = ui.spn_passes.value()
cv_native = "true" if ui.chk_native.isChecked() else "false"
tgt_uv_state = 2 if ui.rdo_uv2.isChecked() else 1 # 1=Replace, 2=Multi-UV
for i, d in enumerate(tgs):
if ui._is_cancelled:
print(f"\n🚨 Bake Cancelado pelo usuario.")
break
ui.pb.setFormat(f"Bake ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i)
QtWidgets.QApplication.processEvents()
obj_name = d['name']
res_str = d['item'].text(3).replace("px", "")
sz = int(res_str) if res_str.isdigit() else res_val
mprint(f"--- Iniciando script V19 portado para: {obj_name} ---")
ms_v19_block = f"""(
fn runBake = (
local objName = "{obj_name}"
local obj = getNodeByName objName
if (obj != undefined and isValidNode obj) then (
select obj
max modify mode
try (
-------------------------------------------------------------------------
-- 1. PREPARE GEOMETRY & UV (V19 Logic 1:1)
-------------------------------------------------------------------------
if (classof obj != Editable_Poly) then convertToPoly obj
local needNewUV = true
if (polyop.getMapSupport obj 2) then needNewUV = false
else ( print "No UVs found on Channel 2. Auto-generating..." )
if needNewUV then (
modPanel.addModToSelection (Unwrap_UVW ()) ui:on
local theMod = obj.modifiers[#Unwrap_UVW]
if theMod != undefined then (
theMod.setMapChannel 2
theMod.flattenMap 45.0 #() 0.01 true 0 true true
theMod.setTVSubObjectMode 3
theMod.selectFaces #{{1..(theMod.numberPolygons())}}
theMod.pack 1 0.001 true false true
collapseStack obj
)
)
-------------------------------------------------------------------------
-- 2. PREPARE BAKE ELEMENTS
-------------------------------------------------------------------------
obj.INodeBakeProperties.removeAllBakeElements()
local be = undefined
local fileExt = ".jpg"
local curRen = renderers.current
local isCorona = matchPattern (curRen as string) pattern:"*Corona*"
local isVRay = matchPattern (curRen as string) pattern:"*V_Ray*"
if isVRay then ( try ( be = VRayCompleteMap() ) catch ( be = CompleteMap() ) )
else if isCorona then ( try ( be = Corona_Beauty() ) catch ( try ( be = CShading_Beauty() ) catch ( be = CompleteMap() ) ) )
else ( be = CompleteMap() )
if be == undefined then be = CompleteMap()
be.outputSzX = {sz}
be.outputSzY = {sz}
be.fileType = fileExt
local pBk = @"{p_bk}"
be.filename = (pBk + obj.name + "_Baked" + fileExt)
try ( be.fileOut = be.filename ) catch()
local fileExists = doesFileExist be.filename
local skipRender = false -- Overwrite always enabled via UI integration
if skipRender then (
print "Skipped (Exists)"
) else (
obj.INodeBakeProperties.addBakeElement be
obj.INodeBakeProperties.bakeEnabled = true
obj.INodeBakeProperties.bakeChannel = 2
if isCorona and ({cv_native} == false) then (
try(renderers.current.pass_limit = {cv_passes}; renderers.current.time_limit = 0; renderers.current.noise_level_limit = 0.0)catch()
)
local wasCancelled = false
try (
print "Ativando Silenciador de Popups (DialogMonitorOPS)..."
DialogMonitorOPS.unRegisterNotification id:#vr4_kill
DialogMonitorOPS.RegisterNotification vr4life_popup_killer id:#vr4_kill
DialogMonitorOPS.enabled = true
if doesFileExist be.filename do ( try ( deleteFile be.filename ) catch() )
-- DESLIGA NA MARRA RENDER DISTRIBUIDO E TEST RESOLUTION DO V-RAY PARA PARAR DE ABORTAR O BAKE
local cR = renderers.current
if (matchPattern (cR as string) pattern:"*V_Ray*") do (
try(cR.system_distributedRender = false)catch()
try(cR.image_testResolution = false)catch()
)
print "Disparando Render API V19..."
-- Fix 2024: explicit outputfile to guarantee disk writing and prevent black/missing files
render rendertype:#bakeSelected vfb:true progressBar:true quiet:true outputSize:[{sz}, {sz}] outputfile:be.filename cancelled:&wasCancelled
) catch (
print "Render Error/Cancel Caught."
wasCancelled = true
)
print "Desativando Silenciador de Popups..."
DialogMonitorOPS.enabled = false
DialogMonitorOPS.unRegisterNotification id:#vr4_kill
if wasCancelled then return -1
)
-------------------------------------------------------------------------
-- 4. MATERIAL CREATION (MOVED TO EXPORT ENGINE P7)
-------------------------------------------------------------------------
local fileFound = false
local waitForFile = 0
while (waitForFile < 50) and (fileFound == false) do (
if doesFileExist be.filename then fileFound = true
else ( sleep 0.1; waitForFile += 1 )
)
if not fileFound do (
print ("ERROR: Texture file not found: " + be.filename)
return -2
)
-------------------------------------------------------------------------
-- 5. FINAL CLEANUP
-------------------------------------------------------------------------
sleep 1.0
-- Preserva o material original da tela e nao suja a viewport!
collapseStack obj
return 1
) catch (
print ("Critical Error on Object: " + obj.name)
print (getCurrentException())
return 0
)
) else (
print "Objeto Invalido"
return 0
)
)
runBake()
)"""
ret = rt.execute(ms_v19_block)
if ret == 1:
d['item'].setText(1, "DONE V19")
d['item'].setForeground(1, QtGui.QColor(0, 255, 0))
mprint(f"-> SUCESSO: Object {obj_name} Finalizado")
elif ret == -1:
d['item'].setText(1, "Cancelado")
ui._is_cancelled = True
elif ret == -2:
d['item'].setText(1, "Tex Missing")
d['item'].setForeground(1, QtGui.QColor(255, 0, 0))
else:
d['item'].setText(1, "Error")
d['item'].setForeground(1, QtGui.QColor(255, 0, 0))
ui.pb.setValue(tot)
ui.pb.setFormat("Bake V19 Concluído!")
print("\n" + "="*50)
print("✅ FIM DO LOG DE BAKE V19")
print("="*50 + "\n")
if auto_export and not ui._is_cancelled:
export_glb_v19(ui)
def export_glb_v19(ui):
mprint("\n" + "="*50)
mprint("🎬 INICIANDO MOTOR DE EXPORT P7: RAW GLB")
p_glb = ui.edt_p_glb.text().replace("\\", "/")
if not p_glb.endswith("/"): p_glb += "/"
if not os.path.exists(p_glb):
try: os.makedirs(p_glb)
except: pass
tgs = ui.get_processable_items()
if not tgs:
mprint("Aviso: Nenhum item valido na lista para exportar.")
return
p_bk = ui.edt_p_bake.text().replace("\\", "/")
if not p_bk.endswith("/"): p_bk += "/"
tgt_uv = 2 if ui.rdo_uv2.isChecked() else 1
if len(tgs) == 1: f_name = f"{tgs[0]['name']}_Export"
else: f_name = "VR4Life_Scene_Export"
full_path = p_glb + f_name + ".glb"
names_str = "#(" + ",".join([f'"{d["name"]}"' for d in tgs]) + ")"
mprint(f"-> Local de Saida: {full_path}")
mprint(f"-> Total de Objetos na Cena: {len(tgs)}")
ms_export = f"""(
fn runExport = (
local objNames = {names_str}
local clones = #()
local errored = false
try (
print "\\n-------------- DIAGNOSTICO DE EXPORTACAO --------------"
for nm in objNames do (
local orig = getNodeByName nm
if orig != undefined do (
local c = copy orig
c.name = orig.name + "_EXP"
convertToPoly c -- Forca o colapso de toda a arvore antes de manipular a UV
-- APLICA O NOVO MATERIAL E A NOVA UV APENAS NO CLONE QUE VAI SER EXPORTADO
local pBk = @"{p_bk}"
local fileExt = ".jpg"
local texPath = (pBk + orig.name + "_Baked" + fileExt)
if doesFileExist texPath do (
local newMat = PhysicalMaterial()
newMat.name = (orig.name + "_GLTF_Mat")
-- GLTF PBR Padrao: Textura limpa no Base Color sem frescuras
newMat.base_color = color 255 255 255
newMat.roughness = 1.0
newMat.emission = 0.0
local bmpTex = BitmapTexture filename:texPath
bmpTex.coords.mapChannel = 1
newMat.base_color_map = bmpTex
c.material = newMat
)
if {tgt_uv} == 2 do (
try ( channelInfo.CopyChannel c 3 2 ) catch()
try ( channelInfo.PasteChannel c 3 1 ) catch()
try ( channelInfo.ClearChannel c 2 ) catch()
)
collapseStack c
append clones c
)
)
if clones.count > 0 then (
print "Invocando Motor Nativo: gltf_export (selectedOnly:true)"
max select none
select clones
local res = exportFile @"{full_path}" #noPrompt selectedOnly:true using:gltf_export
print "Export Finalizado. Limpando cena..."
delete clones
return true
) else (
print "Nenhum arquivo para clonar!"
return false
)
) catch (
print "Critical Error na preparacao do GLB"
print (getCurrentException())
try(delete clones)catch()
return false
)
)
runExport()
)"""
res = rt.execute(ms_export)
if res == True:
mprint("✅ SUCESSO: GLB Gerado com Texturas Embutidas (Modo Silencioso)!")
else:
mprint("🚨 ERRO: Falha na geracao. Vefique o log.")
mprint("="*50 + "\n")
def upd_res_col(ui):
a_r = ui.chk_a_res.isChecked()
m_r = ui.spn_res.value()
for d in ui.bake_items:
o = rt.getNodeByName(d['name'])
if o:
try:
md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z))
r = 256 if md <= ui.s256.value() else 512 if md <= ui.s512.value() else 1024 if md <= ui.s1024.value() else m_r if a_r else m_r
d['item'].setText(3, f"{r}px")
d['item'].setText(4, f"{md:.1f}")
if r == 256: d['item'].setForeground(3, QtGui.QColor(0, 255, 255))
elif r == 512: d['item'].setForeground(3, QtGui.QColor(0, 255, 0))
elif r == 1024: d['item'].setForeground(3, QtGui.QColor(255, 165, 0))
else: d['item'].setForeground(3, QtGui.QColor(255, 50, 50))
except: pass
def get_processable_items(ui):
valid_items = []
for d in ui.bake_items:
obj = rt.getNodeByName(d['name'])
if obj: valid_items.append(d)
return valid_items