🚀 Release Notes - VR4Life Plugin v312

🐛 Correções de Bugs (Bugfixes):

Fix (Bake Engine): Resolvido o erro crítico "ERROR: Texture file not found" (Textura preta ou ausente). Adicionado e forçado o parâmetro outputfile diretamente na API de render do 3ds Max, garantindo que o V-Ray/Corona grave fisicamente o arquivo JPG/PNG no disco.
Fix (Sincronia de UVs): Corrigido o bug onde o "Mapeamento Perfeito" sumia. O botão Prepare UV (P5) estava abrindo a malha no Canal 3, enquanto o render procurava no Canal 2. O processo de Packing foi centralizado definitivamente no Canal 2.
Fix (Destruição de UV na Exportação): O motor de exportação GLB foi severamente refatorado. Ele não rouba mais canais de UV, não altera as emissões do Material PBR e nem colapsa o objeto original da sua cena. Ele agora clona a malha em background, exporta de forma limpa e deleta o clone, mantendo sua Viewport intocada.
 Melhorias e Novas Funcionalidades (Features):

Portabilidade V19 (Core): A lógica raiz, robusta e confiável de colapso de textura do MaxScript (v19 original) foi 100% recriada e injetada com sucesso dentro da estrutura PySide/Python modular atual (

vr4life_engine.py
).
Preservação do Modificador Unwrap: Removido o comando de collapseStack automático ao fim do processo de Bake. Agora a ferramenta de Bake aplica a textura perfeitamente, mas preserva a sua pilha de modificadores para edições futuras no modifier Unwrap UVW.
Interface do Usuário (UI): Título da ferramenta e cabeçalho principal atualizados para refletir a nova estabilidade (Versão movida da V257 para V312 com data e hora de modificação assinadas na tela principal).
main
PauloHNCosta 1 month ago
parent 4886aa2620
commit 5e779cf9ec

@ -0,0 +1,61 @@
import os
from pymxs import runtime as rt
def install_vr4life_2026_fixed():
try:
# 1. CAMINHOS
plugin_dir = os.path.dirname(os.path.realpath(__file__)).replace("\\", "/")
# Onde o MNX foi copiado pelo seu instalador
pasta_macros = rt.getDir(rt.name("userMacros"))
pasta_enu = os.path.dirname(pasta_macros)
caminho_mnx = os.path.join(pasta_enu, "en-US", "UI", "vr4life.mnx").replace("\\", "/")
# 2. REGISTRAR AS MACROS (Obrigatorio para o MNX funcionar)
# Usamos exatamente a categoria "Immerse Games" que está no seu MNX
macro_cmd = f"""
macroScript VR4Life_Launcher
category:"Immerse Games"
tooltip:"Abrir VR4Life"
(
on execute do python.executeFile "{plugin_dir}/run_vr4life.py"
)
macroScript VR4Life_Update
category:"Immerse Games"
tooltip:"Atualizar VR4Life"
(
on execute do python.executeFile "{plugin_dir}/vr4life_updater.py"
)
"""
rt.execute(macro_cmd)
# 3. CARREGAR O MNX VIA INTERFACE DE CONTEXTO (Abordagem Babylon 2026)
# Se o menuMan é undefined, usamos o carregamento de arquivo de configuração
setup_script = f"""
(
local mnxPath = "{caminho_mnx}"
if (doesFileExist mnxPath) then (
-- No 2026, tentamos carregar o arquivo de menus diretamente
-- sem passar pelas propriedades de "getMainMenuBar"
try (
menuMan.loadMenuFile mnxPath
menuMan.updateMenuBar()
) catch (
-- Se falhar aqui, o Max carregará no próximo boot
-- pois o arquivo está na pasta oficial de UI
)
)
)
"""
rt.execute(setup_script)
print(">> VR4Life: Macros registradas e MNX vinculado.")
return True
except Exception as e:
print(f"Erro na instalação: {str(e)}")
return False
if __name__ == "__main__":
install_vr4life_2026_fixed()

@ -0,0 +1,15 @@
import sys, os
import importlib
# Garante que o 3ds Max enxergue a sua pasta de plugins
script_dir = os.path.dirname(os.path.realpath(__file__))
if script_dir not in sys.path:
sys.path.append(script_dir)
# Importa a sua nova UI limpa e recarrega para sempre pegar atualizações
import vr4life_ui
importlib.reload(vr4life_ui)
if __name__ == "__main__":
app = vr4life_ui.AutoBakeManager()
app.show()

@ -0,0 +1,510 @@
macroScript AutoBakeGLB_V45_2
category:"Paulo Tools"
tooltip:"Bake v45.2"
buttonText:"BAKE v45.2"
(
-- Close existing dialogs
try(destroyDialog rolloutBakeGLB)catch()
-- STRUCTURE TO HOLD DATA
struct BakeItemData ( node, listItem )
rollout rolloutBakeGLB "Auto-Bake Manager V45.2" width:600 height:860
(
-- HEADER
label lblTitle "VR4LIFE" align:#center height:30 css:"font-size:30px; font-weight:bold"
label lblVer "v45.2 (Fix Calc)" align:#center height:20 css:"font-size:12px; color:gray" offset:[0,-5]
-- ====================================================================================
-- 1. CONFIGURATION GROUP
-- ====================================================================================
group "1. Output & Tools"
(
edittext edtPath "Folder:" text:"" fieldWidth:360 align:#left across:2
button btnBrowse "..." width:30 height:18 align:#right offset:[0,-2]
edittext edtName "GLB Name:" text:"Scene_Exported" fieldWidth:200 align:#left across:2
button btnOpenFolder "Open Folder" width:120 height:22 align:#right
button btnFix "FIX GEOMETRY (Explode & Poly)" width:280 height:30 css:"background-color:#ffcccc; font-weight:bold" across:2 align:#left
button btnCheck "CHECK & ADD UNWRAP (Ch. 2)" width:280 height:30 align:#right
spinner spnPolyPercent "Reduc. %:" range:[1.0, 100.0, 50.0] type:#float fieldWidth:50 across:3 align:#left offset:[0,4]
spinner spnPolyLimit "Max Verts:" range:[100, 1000000, 40000] type:#integer fieldWidth:60 align:#left offset:[0,4]
button btnReduce "REDUCE (ProOptimizer)" width:160 height:25 align:#right
button btnInspect "INSPECT UV (Select 1)" width:580 height:30 align:#center
)
group "2. Render Configuration"
(
radiobuttons rdoEngine labels:#("V-Ray", "Corona", "Arnold") default:1 columns:3 across:2 align:#left
spinner spnSize "Size:" range:[128, 8192, 512] type:#integer fieldwidth:50 align:#right
spinner spnPasses "Pass Limit:" range:[1, 999, 5] type:#integer fieldwidth:50 across:2 align:#left
checkbox chkUseNative "Use F10 Settings (Ignore Limits)" checked:false offset:[0, 4] align:#right
button btnConfig "Open F10 Config" width:280 height:25 across:2 align:#left
checkbox chkShowVFB "Show Render Window" checked:false offset:[0, 4] align:#right
radiobuttons rdoFinalUV labels:#("UV 1 (Replace)", "UV 2 (Multi-UV)") default:1 columns:2 across:2 align:#left
checkbox chkAutoFlatten "Re-Flatten + Pack" checked:true align:#right
checkbox chkOverwrite "Overwrite Existing Files" checked:false align:#center css:"color:red"
)
-- ====================================================================================
-- 2. LIST MANAGER
-- ====================================================================================
dotNetControl lvObjects "System.Windows.Forms.ListView" width:580 height:350 pos:[10, 350]
-- button btnAddSel "POST SELECTION TO LIST" width:580 height:40 pos:[10, 710] css:"font-weight:bold"
-- button btnClear "CLEAR LIST" width:280 height:40 pos:[310, 710]
progressbar pbProg "Progress:" color:green height:20 width:580 pos:[10, 760]
button btnBakeChecked "BAKE CHECKED ITEMS" width:580 height:50 pos:[10, 790] css:"font-weight:bold; font-size:14px; background-color:#ccffcc"
button btnExportSel "EXPORT SELECTED (1+ Obj)" width:580 height:40 pos:[10, 850]
label lblStatus "Ready" pos:[10, 900] width:580 align:#center
-- LOCAL VARIABLES
-- ====================================================================================
local bakeItems = #()
local userCancelled = false
local ignoreCheckEvent = false
-- ====================================================================================
-- HELPER FUNCTIONS
-- ====================================================================================
fn InitListView = (
lvObjects.View = (dotNetClass "System.Windows.Forms.View").Details
lvObjects.FullRowSelect = true
lvObjects.GridLines = true
lvObjects.CheckBoxes = true
lvObjects.MultiSelect = true
lvObjects.Columns.Add "Object Name" 280
lvObjects.Columns.Add "Status" 120
lvObjects.Columns.Add "Polys" 80
)
fn UpdateItemStatus item status colorCode = (
item.SubItems.Item[1].Text = status
item.ForeColor = colorCode
-- lvObjects.Refresh() -- Refreshing every item causes flicker, max handles it better usually
)
fn AutoPackUV obj channel = (
try (
max modify mode
select obj
modPanel.addModToSelection (Unwrap_UVW ()) ui:on
theMod = obj.modifiers[#Unwrap_UVW]
if theMod != undefined then (
theMod.setMapChannel channel
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
)
) catch ( print ("Pack UV Error: " + obj.name) )
)
fn ExportToGLB objs path = (
if objs.count == 0 then return false
select objs
-- MANUAL EXPORT DIALOG (File Picker)
local filename = getSaveFileName caption:"Export GLB" types:"GL Transmission Format (*.glb)|*.glb|All|*.*" initialDir:path filename:edtName.text
if filename != undefined then (
try ( exportFile filename selectedOnly:true using:gltf_export; return true )
catch ( messageBox "Export Cancelled or Error."; return false )
)
return false
)
fn CloseAnnoyingWindows = (
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 ()
-- try ( windows.sendMessage hwnd 0x0010 0 0 ) catch () -- Alternative Close
)
)
)
) catch ()
)
-- ====================================================================================
-- CORE LOGIC: BAKE ONE ITEM
-- ====================================================================================
fn BakeOneObject objData = (
obj = objData.node
item = objData.listItem
-- Update UI
item.EnsureVisible()
UpdateItemStatus item "Baking..." (dotNetClass "System.Drawing.Color").OrangeRed
-- Validate
if (isValidNode obj == false) then ( UpdateItemStatus item "Deleted" (dotNetClass "System.Drawing.Color").Red; return false)
select obj
max modify mode
try (
-------------------------------------------------------------------------
-- 1. PREPARE GEOMETRY & UV
-------------------------------------------------------------------------
if (classof obj != Editable_Poly) then convertToPoly obj
needNewUV = true
if (chkAutoFlatten.checked == false) then (
if (polyop.getMapSupport obj 2) then needNewUV = false
else ( print "No UVs found on Channel 2. Auto-generating..." )
)
if needNewUV then ( AutoPackUV obj 2; collapseStack obj )
-------------------------------------------------------------------------
-- 2. PREPARE BAKE ELEMENTS
-------------------------------------------------------------------------
obj.INodeBakeProperties.removeAllBakeElements()
be = undefined
fileExt = ".jpg" -- Default
-- Engine Detection
curRen = renderers.current
isCorona = matchPattern (curRen as string) pattern:"*Corona*"
isVRay = matchPattern (curRen as string) pattern:"*V_Ray*"
isArnold = matchPattern (curRen as string) pattern:"*Arnold*"
case rdoEngine.state of (
1: ( try ( be = VRayCompleteMap() ) catch ( ) )
2: ( try ( be = Corona_Beauty() ) catch ( try ( be = CShading_Beauty() ) catch ( ) ) )
3: ( be = CompleteMap() )
)
if be == undefined then be = CompleteMap()
be.outputSzX = spnSize.value
be.outputSzY = spnSize.value
be.fileType = fileExt
-- SMART SKIP: Remove timestamp to allow stable file checking
be.filename = (edtPath.text + obj.name + "_Baked" + fileExt)
fileExists = doesFileExist be.filename
skipRender = (fileExists) and (chkOverwrite.checked == false)
if skipRender then (
UpdateItemStatus item "Skipped (Exists)" (dotNetClass "System.Drawing.Color").Blue
) else (
-- PREPARE RENDER
obj.INodeBakeProperties.addBakeElement be
obj.INodeBakeProperties.bakeEnabled = true
obj.INodeBakeProperties.bakeChannel = 2
-- HARD ENFORCE CORONA LIMITS
if isCorona and (chkUseNative.checked == false) then (
try(renderers.current.pass_limit = spnPasses.value; renderers.current.time_limit = 0; renderers.current.noise_level_limit = 0.0)catch()
)
wasCancelled = false
-- RENDER EXECUTION
try (
render rendertype:#bakeSelected vfb:chkShowVFB.checked progressBar:true quiet:true outputSize:[spnSize.value, spnSize.value] cancelled:&wasCancelled
if wasCancelled then userCancelled = true
) catch (
userCancelled = true
print "Render Error/Cancel Caught."
)
if userCancelled then ( UpdateItemStatus item "Cancelled" (dotNetClass "System.Drawing.Color").Red; return false )
)
-------------------------------------------------------------------------
-- 4. MATERIAL CREATION
-------------------------------------------------------------------------
newMat = PhysicalMaterial()
newMat.name = (obj.name + "_Baked_Mat")
newMat.base_color = color 255 255 255
newMat.roughness = 1.0
newMat.emission = 0.0 -- Lit
newMat.emission_color = color 0 0 0
-- Wait for File Logic
fileFound = false
if (skipRender) then (
fileFound = true -- File existed, we verified it before
) else (
-- We rendered, so we wait/verify
waitForFile = 0
while (waitForFile < 50) and (fileFound == false) do (
if doesFileExist be.filename then fileFound = true
else ( sleep 0.1; waitForFile += 1 )
)
)
if fileFound then (
bmpTex = BitmapTexture filename:be.filename
if rdoFinalUV.state == 2 then bmpTex.coords.mapChannel = 2 else bmpTex.coords.mapChannel = 1
newMat.base_color_map = bmpTex
showTextureMap newMat newMat.base_color_map on
) else (
print ("ERROR: Texture file not found: " + be.filename)
UpdateItemStatus item "Tex Missing" (dotNetClass "System.Drawing.Color").Red
return false
)
-------------------------------------------------------------------------
-- 5. FINAL CLEANUP
-------------------------------------------------------------------------
CloseAnnoyingWindows() -- Kill windows (V43)
sleep 1.0 -- Breathing room
obj.material = newMat
if rdoFinalUV.state == 1 then (
channelInfo.CopyChannel obj 3 2
channelInfo.PasteChannel obj 3 1
channelInfo.ClearChannel obj 2
collapseStack obj
) else ( collapseStack obj )
UpdateItemStatus item "DONE" (dotNetClass "System.Drawing.Color").Green
return true
) catch (
print ("Critical Error on Object: " + obj.name)
print (getCurrentException())
UpdateItemStatus item "Error" (dotNetClass "System.Drawing.Color").Red
return false
)
)
-- ====================================================================================
-- EVENT HANDLERS
-- ====================================================================================
on rolloutBakeGLB open do (
defaultPath = maxFilePath + "_BAKED\\"
if maxFilePath == "" then defaultPath = "C:\\Temp_Bake\\"
edtPath.text = defaultPath
InitListView()
spnPasses.enabled = (not chkUseNative.checked)
)
on lvObjects ItemChecked arg do (
if ignoreCheckEvent then return ()
if arg.Item.Checked then (
ignoreCheckEvent = true
-- Uncheck everyone else
for i = 0 to lvObjects.Items.Count-1 do (
if lvObjects.Items.Item[i] != arg.Item then lvObjects.Items.Item[i].Checked = false
)
ignoreCheckEvent = false
)
)
on lvObjects ItemSelectionChanged arg do (
if arg.IsSelected then (
-- Find node in struct array
local selectedNode = undefined
for b in bakeItems do (
if b.listItem == arg.Item then (
selectedNode = b.node
exit
)
)
if isValidNode selectedNode then (
select selectedNode
-- Optional: max zoomext selected
)
)
)
on chkUseNative changed state do ( spnPasses.enabled = (not state) )
on btnBrowse pressed do (
newPath = getSavePath caption:"Save to:" initialDir:edtPath.text
if newPath != undefined then edtPath.text = (newPath + "\\")
)
on btnOpenFolder pressed do ( ShellLaunch edtPath.text "" )
on btnConfig pressed do ( renderSceneDialog.open() )
on btnFix pressed do (
undo "Fix Geometry" on (
local sel = selection as array
if sel.count == 0 then (messageBox "Select objects in viewport first!"; return false)
local groupHeads = for o in sel where isGroupHead o collect o
for g in groupHeads do ( if isValidNode g then explodeGroup g )
-- Re-select
max modify mode
local finalSel = selection as array
-- AUTO-POPULATE LIST V24 (ADDITIVE)
local countAdded = 0
for obj in finalSel do (
-- STRICT FILTER: Geometry Only, No Lights, No Cameras, No Helpers
if (isValidNode obj) and (superclassof obj == GeometryClass) and (classof obj != TargetObject) and (isGroupHead obj == false) then (
-- Always Ensure Poly (Re-Fix)
if canConvertTo obj Editable_Poly then convertToPoly obj
-- Check for Duplicates
local isDuplicate = false
for b in bakeItems do ( if b.node == obj then isDuplicate = true )
if (not isDuplicate) then (
li = lvObjects.Items.Add obj.name
li.SubItems.Add "Pending"
-- Use getPolygonCount for safety (returns array: #(faces, verts))
local fCount = (getPolygonCount obj)[1]
li.SubItems.Add (fCount as string)
li.Checked = false
append bakeItems (BakeItemData node:obj listItem:li)
countAdded += 1
)
)
)
if countAdded > 0 then messageBox (countAdded as string + " Object(s) Added to List.")
else messageBox "Geometry Re-Fixed (No new items added)."
)
)
on btnCheck pressed do (
undo "Check UVs" on (
objsToCheck = selection as array
local countAdded = 0
max modify mode
for obj in objsToCheck do (
if (superclassof obj == GeometryClass and isGroupHead obj == false) then (
if (classof obj != Editable_Poly) then convertToPoly obj
-- Check if modifier already exists (Ignore internal data support)
local hasModifier = false
for m in obj.modifiers do ( if classof m == Unwrap_UVW then hasModifier = true )
if (not hasModifier) then (
select obj
modPanel.addModToSelection (Unwrap_UVW ()) ui:on
obj.modifiers[#Unwrap_UVW].setMapChannel 2
countAdded += 1
)
)
)
if countAdded > 0 then messageBox ("Unwraps Added to Channel 2: " + countAdded as string)
else messageBox "Unwrap Modifier already present on selected object(s)."
)
)
on btnReduce pressed do (
undo "Reduce Polys" on (
local sel = selection as array
if sel.count == 0 then (messageBox "Select objects first!"; return false)
max modify mode
for obj in sel do (
if (superclassof obj == GeometryClass) and (isGroupHead obj == false) then (
if (classof obj != Editable_Poly) then convertToPoly obj
select obj -- Ensure context
local mods = ProOptimizer()
addModifier obj mods
mods.KeepUV = true -- Keep Textures! Keep UV Boundaries off by default for smooth
mods.OptimizationMode = 1 -- Protect Borders
mods.Calculate = true -- Trigger Calculation Explicitly
-- Use Percentage AFTER Calculation
mods.VertexPercent = spnPolyPercent.value
-- Update List Count if compatible
-- Force refresh of geometry for count
local newFCount = (getPolygonCount obj)[1]
for b in bakeItems do (
if b.node == obj then (
b.listItem.SubItems.Item[2].Text = (newFCount as string)
)
)
)
)
messageBox "ProOptimizer Applied (User Settings) \u0026 List Updated."
)
)
on btnInspect pressed do (
if selection.count != 1 then (messageBox "Select EXACTLY ONE object."; return false)
obj = selection[1]
max modify mode
if (isGroupHead obj) then (messageBox "It is a group! Use Fix Geometry."; return false)
theMod = undefined
for m in obj.modifiers do ( if classof m == Unwrap_UVW then theMod = m )
if theMod != undefined then (
modPanel.setCurrentObject theMod
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
try ( theMod.edit() ) catch ()
) else ( messageBox "No Unwrap found. Use CHECK & ADD first." )
)
on btnBakeChecked pressed do (
userCancelled = false
-- 1. Setup Render Limits GLOBALLY
curRen = renderers.current
isCorona = matchPattern (curRen as string) pattern:"*Corona*"
isVRay = matchPattern (curRen as string) pattern:"*V_Ray*"
isArnold = matchPattern (curRen as string) pattern:"*Arnold*"
makeDir edtPath.text
-- Store old logic similar to V19...
-- For brevity, we assume the BakeOneObject handles the per-object enforcement
-- But we should disable global time limits once here if not native
undo off
(
suspendEditing()
for b in bakeItems do (
if userCancelled then exit
if b.listItem.Checked then (
-- Check if already done? Optional. For now we force re-bake if checked.
success = BakeOneObject b
)
)
resumeEditing()
completeRedraw()
)
lblStatus.text = "Finished!"
)
on btnExportSel pressed do (
if selection.count >= 1 then (
local exportObj = selection as array
ExportToGLB exportObj edtPath.text
messageBox "Export Process Initiated."
) else ( messageBox "Please select AT LEAST ONE object in the viewport." )
)
)
createDialog rolloutBakeGLB height:930
)

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<MaxMenuTransformations>
<CreateTopLevelMenu Id="57652913-7b1a-4282-9e07-4e3e1eecd404" Title="vr4life"/>
<CreateMenuAction MenuId="57652913-7b1a-4282-9e07-4e3e1eecd404" Id="e20491dc-6fa6-4f31-8c69-af132733dcd9" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
<CreateMenuAction MenuId="57652913-7b1a-4282-9e07-4e3e1eecd404" Id="65904065-eee3-45a9-a954-b2eb5f5f3f16" ActionId="647394-VR4Life_Update`Immerse Games"/>
<CreateTopLevelMenu Id="ac98fd17-dda8-4066-aa63-1738a0325d06" Title="Vr4life"/>
<DeleteItem Id="ac98fd17-dda8-4066-aa63-1738a0325d06"/>
<CreateMenu ParentId="b4779ebb-a6f0-4815-9777-57c01c0b584c" MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Title="Vr4Life"/>
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="5fd58e27-1a07-4f84-87af-d274abbc5cf0" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="53e77ac3-9e84-43ad-ae44-9b8a841f5aec" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
<MoveItem DestinationId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" ItemId="53e77ac3-9e84-43ad-ae44-9b8a841f5aec" BeforeId="5fd58e27-1a07-4f84-87af-d274abbc5cf0"/>
<DeleteItem Id="5fd58e27-1a07-4f84-87af-d274abbc5cf0"/>
<DeleteItem Id="53e77ac3-9e84-43ad-ae44-9b8a841f5aec"/>
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="14dc1d27-2b7b-4be7-bb91-a64c076e0bbf" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="61dc0777-6d19-492c-b56c-436dd3d167b0" ActionId="647394-VR4Life_Update`Immerse Games"/>
<SetItemTitle Id="14dc1d27-2b7b-4be7-bb91-a64c076e0bbf" Title="VR4Life Laucher" ResIdTitle="4b96f0b6-f0e6-45aa-aa8e-a2db386f97e5"/>
</MaxMenuTransformations>

@ -0,0 +1,124 @@
import os, time, zipfile
from pymxs import runtime as rt
try: from PySide6 import QtWidgets
except ImportError: from PySide2 import QtWidgets
def mock_connect_api(ui):
if len(ui.e_hash.text()) < 5: QtWidgets.QMessageBox.warning(ui, "Acesso Negado", "Hash inválida."); return
ui.b_conn.setText("Conectando..."); QtWidgets.QApplication.processEvents(); time.sleep(1)
for w in [ui.c_chn, ui.c_pst, ui.e_tit, ui.e_mp3, ui.b_mp3]: w.setEnabled(True); w.setStyleSheet("background: white; color: black; font-weight: bold;")
ui.c_chn.clear(); ui.c_chn.addItems(["Canal: Imóveis Virtuais", "Canal: Showroom Zombisco"])
ui.c_pst.clear(); ui.c_pst.addItems(["[ + Cadastrar Nova Postagem ]", "Editar: Apartamento V1"])
ui.chk_up.setEnabled(True); ui.chk_up.setStyleSheet("color: #FFD700; font-weight: bold;"); ui.chk_up.setChecked(True)
ui.b_conn.setText("✅ Conectado!"); ui.b_conn.setStyleSheet("background: #000; color: #0F0; font-weight: bold; border: 1px solid #0F0;")
on_post_changed(ui, 0)
def on_post_changed(ui, idx):
if idx == 0:
ui.e_tit.setText(""); ui.e_tit.setPlaceholderText("Título do projeto...")
ui.chk_tmb.setChecked(True); ui.chk_tmb.setEnabled(False); ui.chk_tmb.setStyleSheet("color: #00FF00; font-weight: bold;")
else:
ui.e_tit.setText(ui.c_pst.currentText().replace("Editar: ", ""))
ui.chk_tmb.setEnabled(True); ui.chk_tmb.setStyleSheet("color: white; font-weight: bold;")
def finalize_export(ui, p_bk, p_glb):
if not os.path.exists(p_glb): os.makedirs(p_glb)
fo = []; rt.execute("max modify mode"); tgs = ui.get_processable_items()
ui.pb.setFormat("Preparando materiais VR (Multi-ID)..."); QtWidgets.QApplication.processEvents()
for d in tgs:
if ui._is_cancelled: break
if "Bake" in d['item'].text(1) or "Já existe" in d['item'].text(1) or "Mat" in d['item'].text(1):
try:
max_id = rt.execute(f"""(local o = getNodeByName "{d['name']}"; local mid = 1; if o != undefined and isKindOf o Editable_Poly do ( for f = 1 to (polyop.getNumFaces o) do ( local id = polyop.getFaceMatID o f; if id > mid do mid = id ) ); mid)""")
if max_id > 1:
# CONSTRUTOR DE MULTI-MATERIAL (Para objetos que foram processados pelo P4: Multi-UV)
ms_mat = f"""(
local o = getNodeByName "{d['name']}"
local mm = MultiMaterial numsubs:{int(max_id)} name:(o.name + "_MultiMat")
for i = 1 to {int(max_id)} do (
local ip = @"{p_bk}/{d['name']}_B_ID" + (i as string) + ".jpg"
if doesFileExist ip do (
local m = PhysicalMaterial name:(o.name + "_MatID_" + (i as string))
m.base_color_map = BitmapTexture filename:ip
m.roughness = 1.0; m.metalness = 0.0; try(m.reflectivity = 0.0; m.reflection_weight = 0.0)catch()
mm[i] = m
)
)
o.material = mm
)"""
rt.execute(ms_mat)
d['item'].setText(1, "Mat OK (Multi)")
fo.append(rt.getNodeByName(d['name']))
else:
# MATERIAL PADRÃO (Para objetos normais que cabem numa textura só)
ip = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/")
if os.path.exists(ip):
rt.execute(f"""(
local o = getNodeByName "{d['name']}"
if o != undefined do (
local m = PhysicalMaterial name:(o.name + "_Mat"); m.base_color_map = BitmapTexture filename:@"{ip}"
m.roughness = 1.0; m.metalness = 0.0; try(m.reflectivity = 0.0; m.reflection_weight = 0.0)catch()
o.material = m
)
)""")
fo.append(rt.getNodeByName(d['name'])); d['item'].setText(1, "Mat OK")
except Exception as e:
d['item'].setText(1, "Erro Mat"); print(f"Erro Material em {d['name']}: {e}")
if not fo and not ui._is_cancelled:
QtWidgets.QMessageBox.warning(ui, "Exportação Interrompida", "Nenhum objeto validado para exportação.")
ui.pb.setFormat("Pronto"); ui.pb.setValue(0)
return
if fo and not ui._is_cancelled:
rt.select(fo); sn = rt.maxFileName.split(".")[0] if rt.maxFileName else "Cena_VR4LIFE"
og = os.path.join(p_glb, f"{sn}.glb").replace("\\", "/"); oz = os.path.join(p_glb, f"{sn}.zip").replace("\\", "/")
tp = os.path.join(p_glb, f"{sn}_Thumb.jpg").replace("\\", "/"); mp = ui.e_mp3.text(); hm = os.path.exists(mp)
to = False
if ui.chk_tmb.isChecked() and ui.chk_tmb.isEnabled():
ui.pb.setFormat("📸 Thumbnail (1280x720)..."); QtWidgets.QApplication.processEvents()
rt.execute(f"""( local c = getActiveCamera(); if c == undefined do ( local ac = for cam in cameras where (classof cam != TargetObject) collect cam; if ac.count > 0 do c = ac[1] ); try ( if c != undefined then ( render camera:c outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) else ( render outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) ) catch() )""")
if os.path.exists(tp): to = True
ui.pb.setFormat("Limpando Hierarquia e XForm..."); QtWidgets.QApplication.processEvents()
rt.execute("""
(
local sel = selection as array
for o in sel do (
o.parent = undefined
ResetXForm o
collapseStack o
if not isKindOf o Editable_Poly do try(convertToPoly o)catch()
)
)
""")
ui.pb.setFormat("Montando GLB..."); QtWidgets.QApplication.processEvents()
try: rt.exportFile(og, rt.name("noPrompt"), selectedOnly=True, using=rt.GLTFExporter)
except: rt.execute(f'exportFile "{og}" #noPrompt selectedOnly:true')
zo = False
if os.path.exists(og):
ui.pb.setFormat("🗜️ ZIPando GLB..."); QtWidgets.QApplication.processEvents()
try:
with zipfile.ZipFile(oz, 'w', zipfile.ZIP_DEFLATED) as zf: zf.write(og, os.path.basename(og))
zo = True
except: pass
if ui.chk_up.isChecked() and ui.chk_up.isEnabled() and zo:
cs = ui.c_chn.currentText(); ac = "NOVO" if ui.c_pst.currentIndex() == 0 else "ATUALIZAR"; tit = ui.e_tit.text()
ui.pb.setFormat(f"📡 API: Enviando..."); QtWidgets.QApplication.processEvents(); time.sleep(1.5)
msg = f"Sincronizado!\n\n📡 Canal: {cs}\n📝 Ação: {ac}\n🏷️ Titulo: {tit}\n\n📦 ZIP: {os.path.basename(oz)}\n📸 Thumb: {'Sim' if to else 'Não'}\n🎵 Áudio: {'Sim' if hm else 'Não'}"
QtWidgets.QMessageBox.information(ui, "Cloud API", msg)
else:
msg = f"Exportado:\n{og}";
if zo: msg += f"\n\n🗜️ ZIP:\n{oz}"
if to: msg += f"\n\n📸 Thumb:\n{tp}"
QtWidgets.QMessageBox.information(ui, "Sucesso", msg)
ui.pb.setFormat("Pronto"); ui.pb.setValue(0)

@ -0,0 +1,521 @@
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"
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):
ui.tree.clear(); ui.bake_items = []
ms = """( local g = #(); fn ex objs = ( for o in objs do ( if isGroupHead o then ( setGroupOpen o true; ex o.children ) else ( if superclassof o == GeometryClass and (classOf o != TargetObject) and (canConvertTo o Editable_Poly) do appendIfUnique g o ) ) ); ex selection; g )"""
sel = rt.execute(ms)
if not sel: return
tl = [{'name': str(o.name), 'poly': rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0} for o in sel]
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"Carregados: {len(tl)}")
ui.pb.setValue(0)
ui.upd_res_col()
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):
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 slice_large_objects(ui, from_auto=False):
if not ui.bake_items: return
thr = ui.spn_max_sz.value()
tgs = ui.get_processable_items()
tot = len(tgs)
ui.pb.setMaximum(tot)
ui.pb.setValue(0)
rt.execute("max modify mode")
print("\n" + "="*50)
print("🔍 INICIANDO LOG: MULTI-UV (P4)")
print(f"-> Tamanho de corte (Threshold): {thr}")
print("="*50)
for i, d in enumerate(tgs):
if ui._is_cancelled: break
ui.pb.setFormat(f"Multi-UV ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i)
QtWidgets.QApplication.processEvents()
print(f"\n--- Processando objeto: {d['name']} ---")
# CÓDIGO CORRIGIDO: Tudo embrulhado num "fn" (função) nativo para o return funcionar.
ms = f"""(
fn applyMultiMatID objName thrVal = (
local o = getNodeByName objName
if o == undefined do return -1
if not isKindOf o Editable_Poly do convertToPoly o
local dx = abs(o.max.x - o.min.x)
local dy = abs(o.max.y - o.min.y)
local dz = abs(o.max.z - o.min.z)
local md = amax #(dx, dy, dz)
if md <= thrVal do (
for f = 1 to (polyop.getNumFaces o) do polyop.setFaceMatID o f 1
return 1
)
local numChunks = (ceil (md / thrVal)) as integer
if numChunks > 10 do numChunks = 10
local axis = if md == dx then 1 else if md == dy then 2 else 3
local minVal = if axis == 1 then o.min.x else if axis == 2 then o.min.y else o.min.z
local step = md / numChunks
for f = 1 to (polyop.getNumFaces o) do (
local center = polyop.getFaceCenter o f
local val = if axis == 1 then center.x else if axis == 2 then center.y else center.z
local id = (floor ((val - minVal) / step)) as integer + 1
if id > numChunks do id = numChunks
if id < 1 do id = 1
polyop.setFaceMatID o f id
)
return numChunks
)
try ( applyMultiMatID "{d['name']}" {thr} ) catch ( -2 )
)"""
try:
print("-> Executando MaxScript de Fatiamento Lógico...")
r = rt.execute(ms)
print(f"-> Resposta do MaxScript: {r}")
if r == -1:
print("🚨 ERRO: Objeto não encontrado na cena pelo MaxScript.")
d['item'].setText(1, "Erro: Objeto")
elif r == -2:
print("🚨 ERRO: MaxScript falhou internamente (Topologia corrompida?).")
d['item'].setText(1, "Erro: Script")
elif r and int(r) > 1:
print(f"✅ SUCESSO: Objeto dividido em {int(r)} IDs!")
d['item'].setText(1, f"IDs: {int(r)}")
else:
print("-> Objeto menor que a Meta. Mantido como 1 ID.")
d['item'].setText(1, "1 ID (Normal)")
except Exception as e:
d['item'].setText(1, "Erro Python")
print(f"🚨 ERRO CRÍTICO PYTHON no Multi-UV: {e}")
ui.pb.setValue(tot)
ui.pb.setFormat("Multi-IDs Gerados!")
print("\n" + "="*50)
print("✅ FIM DO LOG MULTI-UV")
print("="*50 + "\n")
def prepare_mesh(ui):
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"UV ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i)
QtWidgets.QApplication.processEvents()
o = rt.getNodeByName(d['name'])
if o:
rt.select(o)
try:
ms = f"""(
local o = getNodeByName "{d['name']}"
if not isKindOf o Editable_Poly do convertToPoly o
local m = Unwrap_UVW()
addModifier o m
m.setMapChannel 3
m.setTVSubObjectMode 3
local maxID = 1
for f = 1 to (polyop.getNumFaces o) do (
local id = polyop.getFaceMatID o f
if id > maxID do maxID = id
)
for x = 1 to maxID do (
m.selectByMatID x
local sel = m.getSelectedFaces()
if not sel.isEmpty do (
m.flattenMap 45.0 #() 0.002 true 0 true false
m.pack 1 0.002 true false false
)
)
collapseStack o
)"""
rt.execute(ms)
if not rt.isKindOf(o, rt.Editable_Poly):
rt.convertTo(o, rt.Editable_Poly)
d['item'].setText(1, "UV Packed (C3)")
except:
d['item'].setText(1, "Erro UV")
ui.pb.setValue(tot)
ui.pb.setFormat("UV Pack Concluído!")
def process_bake_logic(ui, auto_export=False):
ui._is_cancelled = False
el = ui.cmb_bake_elem.currentText()
p_bk = ui.edt_p_bake.text()
rnd = str(rt.execute("renderers.current as string"))
i_vr = "V_Ray" in rnd
i_cor = "Corona" in rnd
u_den = ui.chk_denoise.isChecked()
a_res = ui.chk_a_res.isChecked()
if not os.path.exists(p_bk): os.makedirs(p_bk)
tgs = ui.get_processable_items()
tot = len(tgs)
ui.pb.setMaximum(tot)
ui.pb.setValue(0)
print("\n" + "="*50)
print("🎬 INICIANDO LOG: MOTOR DE BAKE (P6)")
print(f"-> Pasta Alvo: {p_bk}")
print(f"-> Renderizador: {rnd}")
print("="*50)
for i, d in enumerate(tgs):
if ui._is_cancelled: break
ui.pb.setFormat(f"Bake ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i)
QtWidgets.QApplication.processEvents()
print(f"\n--- Preparando Bake: {d['name']} ---")
o = rt.getNodeByName(d['name'])
if not o:
print("🚨 ERRO: Objeto não encontrado.")
continue
try:
# 1. Conta quantos IDs existem no objeto
ms_mid = f"""(local o = getNodeByName "{d['name']}"; local mid = 1; if o != undefined and isKindOf o Editable_Poly do ( for f = 1 to (polyop.getNumFaces o) do ( local id = polyop.getFaceMatID o f; if id > mid do mid = id ) ); mid)"""
max_id = rt.execute(ms_mid)
if not max_id: max_id = 1
max_id = int(max_id)
print(f"-> Total de IDs a renderizar: {max_id}")
rt.execute("max select none")
rt.select(o)
rt.execute("max modify mode")
rt.execute("max zoomext sel all")
rt.redrawViews()
time.sleep(0.5)
for mid in range(1, max_id + 1):
if ui._is_cancelled: break
print(f"\n-> Assando ID: {mid} de {max_id}")
if max_id == 1:
t_jpg_id = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/")
else:
t_jpg_id = os.path.join(p_bk, f"{d['name']}_B_ID{mid}.jpg").replace("\\", "/")
if os.path.exists(t_jpg_id):
d['item'].setText(1, f"Já existe ID{mid}")
print("-> Ficheiro já existe. Pulando.")
continue
cr = ui.spn_res.value()
if a_res:
md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z))
cr = 256 if md <= ui.s256.value() else 512 if md <= ui.s512.value() else 1024 if md <= ui.s1024.value() else cr
ext = ".exr" if (i_vr and u_den) else ".jpg"
t_rnd_id = t_jpg_id.replace(".jpg", ".exr") if ext == ".exr" else t_jpg_id
if max_id > 1:
print("-> Isolando UV do ID atual...")
# CÓDIGO CORRIGIDO 1: Embrulhado em 'fn'
ms_uv = f"""(
fn isolateUV currentMid = (
global temp_uv_mod = Unwrap_UVW()
addModifier $ temp_uv_mod
temp_uv_mod.setMapChannel 3
temp_uv_mod.setTVSubObjectMode 3
local allF = #{{1..(polyop.getNumFaces $)}}
temp_uv_mod.selectByMatID currentMid
local tgtF = temp_uv_mod.getSelectedFaces()
local hideF = allF - tgtF
if hideF.numberset > 0 do (
temp_uv_mod.selectFaces hideF
temp_uv_mod.moveSelected [-10, -10, 0]
)
return 1
)
try ( isolateUV {mid} ) catch ( 0 )
)"""
res_uv = rt.execute(ms_uv)
if res_uv == 0: print("🚨 ERRO: O MaxScript não conseguiu manipular as UVs.")
print(f"-> Disparando o Render: {t_rnd_id}")
# CÓDIGO CORRIGIDO 2: Embrulhado em 'fn'
ms_bake = f"""(
fn doBake fileOut szX szY fileExt cor den elem = (
try(freeSceneBitmaps(); vfbControl #clearimage)catch()
if cor and den do try ( renderers.current.denoise_enable = true ) catch()
$.INodeBakeProperties.removeAllBakeElements()
local be = (execute (elem + "()"))
be.outputSzX = szX
be.outputSzY = szY
be.fileType = fileExt
be.fileName = fileOut
$.INodeBakeProperties.addBakeElement be
$.INodeBakeProperties.bakeEnabled = true
$.INodeBakeProperties.bakeChannel = 3
render rendertype:#bakeSelected vfb:true quiet:true production:true outputfile:fileOut
return 1
)
try ( doBake "{t_rnd_id}" {cr} {cr} "{ext}" {str(i_cor).lower()} {str(u_den).lower()} "{el}" ) catch ( 0 )
)"""
res_bk = rt.execute(ms_bake)
if res_bk == 0: print("🚨 ERRO: O Renderizador rejeitou o comando de Bake!")
wt = 0
while not os.path.exists(t_rnd_id) and wt < 60:
time.sleep(0.5)
wt += 0.5
QtWidgets.QApplication.processEvents()
if os.path.exists(t_rnd_id):
if i_vr and u_den:
print("-> Passando Denoise IA...")
c_in = t_rnd_id.replace("/", "\\")
c_out = t_rnd_id.replace(".exr", "_denoised.exr").replace("/", "\\")
try:
p = subprocess.Popen(f'{get_vdenoise_path()} -inputFile="{c_in}" -outputFile="{c_out}" -display=0', shell=True, creationflags=0x08000000); dt = 0
while p.poll() is None and dt < 60: QtWidgets.QApplication.processEvents(); time.sleep(0.5); dt += 0.5
if p.poll() is None: p.terminate()
except: pass
f_exr = c_out.replace("\\", "/") if os.path.exists(c_out.replace("\\", "/")) else t_rnd_id
rt.execute(f"""( try(vfbControl #clearimage)catch(); try ( local bI = openBitmap @"{f_exr}"; if bI != undefined do ( local bO = bitmap bI.width bI.height filename:@"{t_jpg_id}" hdr:true; copy bI bO; save bO; close bI; close bO; free bI; free bO ) ) catch() )""")
rt.execute("freeSceneBitmaps(); gc light:true"); time.sleep(0.5)
try: os.remove(t_rnd_id); os.remove(c_out.replace("\\", "/"))
except: pass
if max_id > 1:
print("-> Removendo modificador temporário de UV...")
rt.execute("try(deleteModifier $ globalvars.get #temp_uv_mod)catch()")
if os.path.exists(t_jpg_id):
d['item'].setText(1, "Bake OK")
print("-> SUCESSO: Textura gravada no disco.")
else:
d['item'].setText(1, "Erro Conv")
print("🚨 ERRO: A textura não foi salva. Timeout?")
if not ui._is_cancelled:
print("-> Restaurando UV final do objeto...")
rt.execute(f"""( local o = getNodeByName "{d['name']}"; try( ChannelInfo.CopyChannel o 3 3; ChannelInfo.PasteChannel o 3 1; collapseStack o )catch() )""")
except Exception as e:
d['item'].setText(1, "Erro Render")
print(f"🚨 EXCEÇÃO PYTHON: {e}")
ui.pb.setValue(tot)
ui.pb.setFormat("Bake Concluído!")
print("\n" + "="*50)
print("✅ FIM DO LOG DE BAKE")
print("="*50 + "\n")
if auto_export and not ui._is_cancelled:
cld.finalize_export(ui, p_bk, ui.edt_p_glb.text())
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

@ -0,0 +1,144 @@
import os
from pymxs import runtime as rt
try: from PySide6 import QtWidgets, QtCore, QtGui
except ImportError: from PySide2 import QtWidgets, QtCore, QtGui
import vr4life_engine as eng
import vr4life_cloud as cld
import importlib
importlib.reload(eng)
importlib.reload(cld)
def get_max_window_safe():
try:
if hasattr(rt, 'getMAXWindow'): return rt.getMAXWindow()
except: pass
for w in QtWidgets.QApplication.topLevelWidgets():
if w.inherits("QMainWindow") or w.windowTitle().startswith("Autodesk 3ds Max"): return w
return None
class AutoBakeManager(QtWidgets.QDialog):
def __init__(self):
super(AutoBakeManager, self).__init__(get_max_window_safe())
self.setWindowTitle("VR4LIFE AUTO-BAKE V167.0 - MODULAR ENTERPRISE")
self.resize(1050, 1380); self.bake_items = []; self._is_cancelled = False
p = self.palette(); p.setColor(QtGui.QPalette.Window, QtGui.QColor(43, 43, 43)); self.setPalette(p); self.setAutoFillBackground(True)
self.init_ui()
rt.clearListener(); eng.load_bake_elements(self)
def init_ui(self):
layout = QtWidgets.QVBoxLayout(self)
lbl_t = QtWidgets.QLabel("VR4LIFE - V167.0 (Modular)"); lbl_t.setAlignment(QtCore.Qt.AlignCenter); lbl_t.setStyleSheet("font-size: 24px; font-weight: bold; color: #00FF00;")
layout.addWidget(lbl_t)
self.tabs = QtWidgets.QTabWidget()
self.tabs.setStyleSheet("QTabWidget::pane { border: 1px solid #777; background: #333; } QTabBar::tab { background: #444; color: #CCC; padding: 10px 15px; font-weight: bold; border: 1px solid #222; } QTabBar::tab:selected { background: #555; color: #00FF00; } QLabel { color: #FFF; font-weight: bold; }")
# ABA 1
t_gen = QtWidgets.QWidget(); l_gen = QtWidgets.QVBoxLayout(t_gen); f_gen = QtWidgets.QFormLayout()
self.btn_ref = QtWidgets.QPushButton("🔄 RECARREGAR MAPAS"); self.btn_ref.setStyleSheet("background: #555; color: white; height: 35px;"); self.btn_ref.clicked.connect(lambda: eng.load_bake_elements(self))
self.cmb_bake_elem = QtWidgets.QComboBox(); self.cmb_bake_elem.setStyleSheet("background: white; color: black; font-weight: bold;")
self.chk_denoise = QtWidgets.QCheckBox("✨ Ativar Denoiser IA"); self.chk_denoise.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_denoise.setChecked(True)
self.edt_p_bake = QtWidgets.QLineEdit(rt.maxFilePath + "_BAKE_JPG\\" if rt.maxFilePath else "C:\\VR4_BAKE\\"); self.edt_p_bake.setStyleSheet("background: white; color: black;")
self.edt_p_glb = QtWidgets.QLineEdit(rt.maxFilePath + "_EXPORT_GLB\\" if rt.maxFilePath else "C:\\VR4_GLB\\"); self.edt_p_glb.setStyleSheet("background: white; color: black;")
self.btn_brw_glb = QtWidgets.QPushButton("..."); self.btn_brw_glb.setStyleSheet("background: #777; color: white; font-weight: bold;"); self.btn_brw_glb.clicked.connect(self.browse_glb_folder)
h_glb = QtWidgets.QHBoxLayout(); h_glb.addWidget(self.edt_p_glb); h_glb.addWidget(self.btn_brw_glb)
f_gen.addRow("Mapa:", self.cmb_bake_elem); f_gen.addRow("", self.chk_denoise); f_gen.addRow("Pasta JPG:", self.edt_p_bake); f_gen.addRow("Pasta GLB/ZIP:", h_glb)
l_gen.addWidget(self.btn_ref); l_gen.addLayout(f_gen); l_gen.addStretch(); self.tabs.addTab(t_gen, "🗂️ 1. Geral")
# ABA 2
t_geo = QtWidgets.QWidget(); l_geo = QtWidgets.QVBoxLayout(t_geo); f_geo = QtWidgets.QFormLayout()
self.chk_a_weld = QtWidgets.QCheckBox("Fundir Grupos na opção 'AUTO'"); self.chk_a_weld.setStyleSheet("color: #00FF00; font-weight: bold;"); self.chk_a_weld.setChecked(True)
self.chk_a_super = QtWidgets.QCheckBox("Super Solda na opção 'AUTO'"); self.chk_a_super.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_a_super.setChecked(False)
self.spn_pct = QtWidgets.QDoubleSpinBox(); self.spn_pct.setStyleSheet("background: white; color: black;"); self.spn_pct.setRange(0.1, 100.0); self.spn_pct.setValue(90.0)
self.spn_min_poly = QtWidgets.QSpinBox(); self.spn_min_poly.setStyleSheet("background: white; color: black;"); self.spn_min_poly.setRange(50, 10000000); self.spn_min_poly.setValue(3000)
self.spn_max_sz = QtWidgets.QDoubleSpinBox(); self.spn_max_sz.setStyleSheet("background: white; color: black;"); self.spn_max_sz.setRange(10.0, 100000.0); self.spn_max_sz.setValue(800.0)
self.chk_a_slice = QtWidgets.QCheckBox("Incluir Multi-UV no 'AUTO'"); self.chk_a_slice.setStyleSheet("color: white; font-weight: bold;")
f_geo.addRow(QtWidgets.QLabel("🧩 SOLDADOR:")); f_geo.addRow("", self.chk_a_weld); f_geo.addRow("", self.chk_a_super)
f_geo.addRow(QtWidgets.QLabel("🛠️ OTIMIZADOR:")); f_geo.addRow("Vertex %:", self.spn_pct); f_geo.addRow("Meta Polys:", self.spn_min_poly); f_geo.addRow(QtWidgets.QLabel("✂️ MULTI-UV:")); f_geo.addRow("Criar IDs se >:", self.spn_max_sz); f_geo.addRow("", self.chk_a_slice)
l_geo.addLayout(f_geo); l_geo.addStretch(); self.tabs.addTab(t_geo, "📐 2. Geometria")
# ABA 3 e 4 mantêm-se iguais
t_tex = QtWidgets.QWidget(); l_tex = QtWidgets.QVBoxLayout(t_tex); f_tex = QtWidgets.QFormLayout()
self.spn_res = QtWidgets.QSpinBox(); self.spn_res.setStyleSheet("background: white; color: black;"); self.spn_res.setRange(128, 8192); self.spn_res.setValue(2048)
self.chk_a_res = QtWidgets.QCheckBox("Auto-Size"); self.chk_a_res.setStyleSheet("color: #FFD700; font-weight: bold;"); self.chk_a_res.setChecked(True)
self.s256 = QtWidgets.QDoubleSpinBox(); self.s256.setStyleSheet("background: white; color: black;"); self.s256.setRange(0.1, 100000.0); self.s256.setValue(100.0); self.s256.setPrefix("< 256px: ")
self.s512 = QtWidgets.QDoubleSpinBox(); self.s512.setStyleSheet("background: white; color: black;"); self.s512.setRange(0.1, 100000.0); self.s512.setValue(300.0); self.s512.setPrefix("< 512px: ")
self.s1024 = QtWidgets.QDoubleSpinBox(); self.s1024.setStyleSheet("background: white; color: black;"); self.s1024.setRange(0.1, 100000.0); self.s1024.setValue(600.0); self.s1024.setPrefix("< 1024px: ")
h_sz = QtWidgets.QHBoxLayout(); h_sz.addWidget(self.chk_a_res); h_sz.addWidget(self.s256); h_sz.addWidget(self.s512); h_sz.addWidget(self.s1024)
for w in [self.chk_a_res, self.s256, self.s512, self.s1024, self.spn_res]: w.valueChanged.connect(self.upd_res_col) if hasattr(w, 'valueChanged') else w.toggled.connect(self.upd_res_col)
f_tex.addRow("Res Máx (>):", self.spn_res); f_tex.addRow("Escala:", h_sz)
l_tex.addLayout(f_tex); l_tex.addStretch(); self.tabs.addTab(t_tex, "🎨 3. Textura")
t_cld = QtWidgets.QWidget(); l_cld = QtWidgets.QVBoxLayout(t_cld); f_cld = QtWidgets.QFormLayout()
self.e_hash = QtWidgets.QLineEdit(); self.e_hash.setStyleSheet("background: white; color: black; letter-spacing: 2px;"); self.e_hash.setEchoMode(QtWidgets.QLineEdit.Password)
self.b_conn = QtWidgets.QPushButton("🔄 Conectar CMS"); self.b_conn.setStyleSheet("background: #2E8B57; color: white; height: 30px;"); self.b_conn.clicked.connect(lambda: cld.mock_connect_api(self))
self.c_chn = QtWidgets.QComboBox(); self.c_chn.setStyleSheet("background: #CCC; color: black;"); self.c_chn.setEnabled(False)
self.c_pst = QtWidgets.QComboBox(); self.c_pst.setStyleSheet("background: #CCC; color: black;"); self.c_pst.setEnabled(False); self.c_pst.currentIndexChanged.connect(lambda idx: cld.on_post_changed(self, idx))
self.e_tit = QtWidgets.QLineEdit(); self.e_tit.setStyleSheet("background: #CCC; color: black;"); self.e_tit.setEnabled(False)
self.e_mp3 = QtWidgets.QLineEdit(); self.e_mp3.setStyleSheet("background: #CCC; color: black;"); self.e_mp3.setEnabled(False)
self.b_mp3 = QtWidgets.QPushButton("🎵 MP3"); self.b_mp3.setStyleSheet("background: #555; color: white;"); self.b_mp3.setEnabled(False); self.b_mp3.clicked.connect(self.browse_mp3)
h_mp3 = QtWidgets.QHBoxLayout(); h_mp3.addWidget(self.e_mp3); h_mp3.addWidget(self.b_mp3)
self.chk_tmb = QtWidgets.QCheckBox("📸 Thumbnail"); self.chk_tmb.setStyleSheet("color: gray; font-weight: bold;"); self.chk_tmb.setEnabled(False)
self.chk_up = QtWidgets.QCheckBox("🚀 Upload Multipart Automático"); self.chk_up.setStyleSheet("color: gray; font-weight: bold;"); self.chk_up.setEnabled(False)
f_cld.addRow("Chave:", self.e_hash); f_cld.addRow("", self.b_conn); f_cld.addRow("Canal:", self.c_chn); f_cld.addRow("Ação:", self.c_pst); f_cld.addRow("Título:", self.e_tit); f_cld.addRow("Som:", h_mp3)
f_cld.addRow("", self.chk_tmb); f_cld.addRow("", self.chk_up)
l_cld.addLayout(f_cld); l_cld.addStretch(); self.tabs.addTab(t_cld, "☁️ 4. API Cloud")
layout.addWidget(self.tabs)
self.btn_full = QtWidgets.QPushButton("🚀 EXECUTAR TUDO (AUTO)"); self.btn_full.setStyleSheet("background: #00A8FF; color: white; font-weight: bold; font-size: 15px; height: 50px; border-radius: 8px;")
self.btn_full.clicked.connect(self.run_full_auto); layout.addWidget(self.btn_full)
self.tree = QtWidgets.QTreeWidget(); self.tree.setStyleSheet("background: white; color: black;"); self.tree.setHeaderLabels(["Objeto", "Status", "Polys", "Res", "Eixo"])
self.tree.setColumnWidth(0, 260); self.tree.setColumnWidth(1, 140); self.tree.setColumnWidth(2, 70); self.tree.setColumnWidth(3, 70); self.tree.setColumnWidth(4, 70); self.tree.itemClicked.connect(self.on_item_click)
layout.addWidget(self.tree)
self.pb = QtWidgets.QProgressBar(); self.pb.setAlignment(QtCore.Qt.AlignCenter); self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: black; background: white; } QProgressBar::chunk { background: #00FF00; }"); self.pb.setFormat("Pronto")
layout.addWidget(self.pb)
h_l = QtWidgets.QHBoxLayout()
self.b1 = QtWidgets.QPushButton("P1: Lista"); self.b1.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b1.clicked.connect(lambda: eng.load_selection(self))
self.b2 = QtWidgets.QPushButton("P2: Fundir"); self.b2.setStyleSheet("background: #32CD32; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2.clicked.connect(lambda: eng.attach_grouped_objects(self, False))
self.b2_5 = QtWidgets.QPushButton("P2.5: Super Solda"); self.b2_5.setStyleSheet("background: #008080; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2_5.clicked.connect(lambda: eng.super_attach_objects(self, False))
self.b3 = QtWidgets.QPushButton("P3: Opt"); self.b3.setStyleSheet("background: #FF8C00; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b3.clicked.connect(lambda: eng.optimize_geometry(self))
self.b4 = QtWidgets.QPushButton("P4: Multi-UV"); self.b4.setStyleSheet("background: #8B008B; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b4.clicked.connect(lambda: eng.slice_large_objects(self, False))
self.b5 = QtWidgets.QPushButton("P5: UV"); self.b5.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b5.clicked.connect(lambda: eng.prepare_mesh(self))
self.b6 = QtWidgets.QPushButton("P6: Bake"); self.b6.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b6.clicked.connect(lambda: eng.process_bake_logic(self, False))
self.b7 = QtWidgets.QPushButton("P7: Exportar"); self.b7.setStyleSheet("background: #FFD700; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b7.clicked.connect(lambda: cld.finalize_export(self, self.edt_p_bake.text(), self.edt_p_glb.text()))
for b in [self.b1, self.b2, self.b2_5, self.b3, self.b4, self.b5, self.b6, self.b7]: h_l.addWidget(b)
layout.addLayout(h_l)
self.btn_cancel = QtWidgets.QPushButton("CANCELAR / FECHAR"); self.btn_cancel.setStyleSheet("background: #FF0000; color: white; font-weight: bold; height: 40px; border-radius: 8px;"); self.btn_cancel.clicked.connect(self.cancel_all)
layout.addWidget(self.btn_cancel)
def browse_glb_folder(self):
f = QtWidgets.QFileDialog.getExistingDirectory(self, "Selecione a Pasta GLB")
if f: self.edt_p_glb.setText(f + "\\")
def browse_mp3(self):
f, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Selecione o Áudio", "", "MP3 (*.mp3)")
if f: self.e_mp3.setText(f); self.e_mp3.setStyleSheet("background: white; color: green; font-weight: bold;")
def upd_res_col(self): eng.upd_res_col(self)
def get_processable_items(self): return eng.get_processable_items(self)
def on_item_click(self, item, column):
o = rt.getNodeByName(item.text(0))
if o: rt.select(o); rt.redrawViews()
def run_full_auto(self):
self._is_cancelled = False
if not rt.execute("selection as array"): QtWidgets.QMessageBox.warning(self, "Aviso", "Selecione algo!"); return
if self.chk_a_weld.isChecked() and not self._is_cancelled: eng.attach_grouped_objects(self, True)
if self.chk_a_super.isChecked() and not self._is_cancelled: eng.super_attach_objects(self, True)
eng.load_selection(self)
if not self.get_processable_items(): return
if self.bake_items and not self._is_cancelled: eng.optimize_geometry(self)
if self.chk_a_slice.isChecked() and not self._is_cancelled: eng.slice_large_objects(self, True)
if not self._is_cancelled: eng.prepare_mesh(self)
if not self._is_cancelled: time.sleep(1.0); eng.process_bake_logic(self, True)
def cancel_all(self): self._is_cancelled = True; self.close()

@ -0,0 +1,92 @@
import os
import urllib.request
from pymxs import runtime as rt
try: from PySide6 import QtWidgets
except ImportError: from PySide2 import QtWidgets
# URL Oficial do repositório da Immerse Games (Lendo a branch 'main')
# Obs: Se o seu Gitea estiver usando 'master' em vez de 'main', basta trocar a última palavra.
GITEA_RAW_URL = "https://git.immersegame.com/immersegame/vr4life-3dmax-plugin/raw/branch/main/"
# Token de Leitura do Gitea
GITEA_TOKEN = "efebcde14ce96a2b80d0b3f207991bc155018ab8"
FILES_TO_UPDATE = [
"vr4life_ui.py",
"vr4life_engine.py",
"vr4life_cloud.py",
"run_vr4life.py",
"vr4life_updater.py"
]
def check_and_update(ui_parent=None):
script_dir = os.path.dirname(os.path.realpath(__file__))
local_version_file = os.path.join(script_dir, "version.txt").replace("\\", "/")
if ui_parent:
ui_parent.pb.setFormat("Autenticando e checando versão no Gitea..."); QtWidgets.QApplication.processEvents()
try:
# 1. Lê a versão local (Se não existir, assume 0.0)
local_version = "0.0"
if os.path.exists(local_version_file):
with open(local_version_file, "r") as f:
local_version = f.read().strip()
# 2. Bate na Nuvem com o Token para ler o version.txt
remote_version_url = GITEA_RAW_URL + "version.txt"
req_version = urllib.request.Request(remote_version_url)
req_version.add_header("Authorization", f"token {GITEA_TOKEN}")
response_version = urllib.request.urlopen(req_version)
remote_version = response_version.read().decode('utf-8').strip()
# 3. Compara as versões: Só baixa se a do Gitea for mais nova
if remote_version == local_version:
if ui_parent:
ui_parent.pb.setFormat("Sistema já está atualizado!"); ui_parent.pb.setValue(100)
QtWidgets.QMessageBox.information(ui_parent, "Atualizador", f"Você já está usando a versão mais recente ({local_version}).")
return
# 4. Inicia o Download Seguro dos Arquivos
if ui_parent:
ui_parent.pb.setFormat(f"Nova versão {remote_version} encontrada! Baixando..."); QtWidgets.QApplication.processEvents()
updated_count = 0
for file_name in FILES_TO_UPDATE:
remote_url = GITEA_RAW_URL + file_name
local_path = os.path.join(script_dir, file_name).replace("\\", "/")
if ui_parent:
ui_parent.pb.setFormat(f"Baixando {file_name}..."); QtWidgets.QApplication.processEvents()
# Puxa o código com o Token
req = urllib.request.Request(remote_url)
req.add_header("Authorization", f"token {GITEA_TOKEN}")
response = urllib.request.urlopen(req)
remote_code = response.read().decode('utf-8')
# Sobrescreve na máquina local
with open(local_path, "w", encoding="utf-8") as f:
f.write(remote_code)
updated_count += 1
# 5. Salva a nova versão local para a próxima checagem
with open(local_version_file, "w") as f:
f.write(remote_version)
msg = f"Sucesso! Plugin atualizado para a versão {remote_version} ({updated_count} arquivos).\n\nPor favor, feche esta janela e abra o plugin novamente pelo menu superior."
if ui_parent:
ui_parent.pb.setFormat("Atualização Concluída!"); ui_parent.pb.setValue(100)
QtWidgets.QMessageBox.information(ui_parent, "Update VR4Life", msg)
else:
rt.messageBox(msg, title="Update VR4Life")
except Exception as e:
erro_msg = f"Falha de Autenticação ou Download.\nVerifique seu Token e URL do Gitea.\n\nDetalhe técnico: {str(e)}"
if ui_parent:
QtWidgets.QMessageBox.critical(ui_parent, "Erro de Update", erro_msg)
ui_parent.pb.setFormat("Pronto"); ui_parent.pb.setValue(0)
else:
rt.messageBox(erro_msg, title="Erro de Update")

@ -0,0 +1,45 @@
macroScript Launch_VR4Life
category:"VR4Life"
tooltip:"VR4Life Auto-Bake Plugin"
buttonText:"Ligar VR4Life"
(
on execute do (
python.ExecuteFile @"Z:\Emmersive games\3dxMAX\vr4life-3dmax-plugin\run_vr4life.py"
)
)
macroScript Reload_VR4Life
category:"VR4Life"
tooltip:"Salvar Estado e Recarregar Plugin"
buttonText:"Recarregar VR4Life"
(
on execute do (
python.Execute "import pymxs\ntry:\n pymxs.runtime.vr4_app.reload_plugin()\nexcept:\n pymxs.runtime.python.ExecuteFile(r'Z:\\Emmersive games\\3dxMAX\\vr4life-3dmax-plugin\\run_vr4life.py')"
)
)
(
local mainMenu = menuMan.getMainMenuBar()
local oldMenu = menuMan.findMenu "VR4Life"
if oldMenu != undefined do (
menuMan.unRegisterMenu oldMenu
)
local vr4Menu = menuMan.createMenu "VR4Life"
local launchItem = menuMan.createActionItem "Launch_VR4Life" "VR4Life"
vr4Menu.addItem launchItem -1
local sep = menuMan.createSeparatorItem()
vr4Menu.addItem sep -1
local reloadItem = menuMan.createActionItem "Reload_VR4Life" "VR4Life"
vr4Menu.addItem reloadItem -1
local topLevelItem = menuMan.createSubMenuItem "VR4Life" vr4Menu
mainMenu.addItem topLevelItem -1
menuMan.updateMenuBar()
messageBox "Menu VR4Life atualizado! Agora a opcao de Recarregar foi transferida para ca." title:"VR4Life Toolbar"
)

@ -11,5 +11,7 @@ import vr4life_ui
importlib.reload(vr4life_ui) importlib.reload(vr4life_ui)
if __name__ == "__main__": if __name__ == "__main__":
from pymxs import runtime as rt
app = vr4life_ui.AutoBakeManager() app = vr4life_ui.AutoBakeManager()
rt.vr4_app = app
app.show() app.show()

@ -0,0 +1 @@
[{"name": "ETICHETTA", "status": "DONE V19", "polys": "5,966", "res": "256px", "id": "60.0"}, {"name": "Object003", "status": "DONE V19", "polys": "7,470", "res": "1024px", "id": "2394.0"}, {"name": "ETICHE_005", "status": "DONE V19", "polys": "2,977", "res": "256px", "id": "56.0"}, {"name": "IMBOTT_002", "status": "DONE V19", "polys": "6,628", "res": "1024px", "id": "2751.4"}, {"name": "Object005", "status": "DONE V19", "polys": "2,846", "res": "1024px", "id": "2456.7"}, {"name": "Object004", "status": "DONE V19", "polys": "2,962", "res": "1024px", "id": "1377.5"}, {"name": "Object006", "status": "DONE V19", "polys": "2,878", "res": "1024px", "id": "764.3"}, {"name": "CUCITURE", "status": "DONE V19", "polys": "936", "res": "256px", "id": "65.8"}]

@ -11,6 +11,37 @@ def get_vdenoise_path():
if os.path.exists(p): return f'"{p}"' if os.path.exists(p): return f'"{p}"'
return "vdenoise.exe" 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): def load_bake_elements(ui):
ui.cmb_bake_elem.clear() ui.cmb_bake_elem.clear()
found = ["CompleteMap", "VRayCompleteMap", "Corona_Beauty", "CShading_Beauty"] found = ["CompleteMap", "VRayCompleteMap", "Corona_Beauty", "CShading_Beauty"]
@ -28,21 +59,66 @@ def load_bake_elements(ui):
except: pass except: pass
def load_selection(ui): def load_selection(ui):
ui.tree.clear(); ui.bake_items = [] mprint("======== [VR4Life] GATILHO DA INTERFACE: P1 [Lista] ========")
ms = """( local g = #(); fn ex objs = ( for o in objs do ( if isGroupHead o then ( setGroupOpen o true; ex o.children ) else ( if superclassof o == GeometryClass and (classOf o != TargetObject) and (canConvertTo o Editable_Poly) do appendIfUnique g o ) ) ); ex selection; g )""" 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) sel = rt.execute(ms)
if not sel: return
tl = [{'name': str(o.name), 'poly': rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0} for o in sel] 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) tl.sort(key=lambda x: x['poly'], reverse=True)
for d in tl: for d in tl:
i = QtWidgets.QTreeWidgetItem([d['name'], "Pronto", f"{d['poly']:,}", "", ""]) i = QtWidgets.QTreeWidgetItem([d['name'], "Pronto", f"{d['poly']:,}", "", ""])
i.setFlags(i.flags() | QtCore.Qt.ItemIsUserCheckable) i.setFlags(i.flags() | QtCore.Qt.ItemIsUserCheckable)
i.setCheckState(0, QtCore.Qt.Checked) i.setCheckState(0, QtCore.Qt.Checked)
ui.tree.addTopLevelItem(i) ui.tree.addTopLevelItem(i)
ui.bake_items.append({'name': d['name'], 'item': i}) ui.bake_items.append({'name': d['name'], 'item': i})
ui.pb.setFormat(f"Carregados: {len(tl)}")
ui.pb.setFormat(f"Adicionados: {len(tl)} / Total: {len(ui.bake_items)}")
ui.pb.setValue(0) ui.pb.setValue(0)
ui.upd_res_col() 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): def attach_grouped_objects(ui, from_auto=False):
ui.pb.setFormat("Fundindo grupos inteiros...") ui.pb.setFormat("Fundindo grupos inteiros...")
@ -123,7 +199,9 @@ def super_attach_objects(ui, from_auto=False):
except Exception as e: print("Erro Super Solda:", e) except Exception as e: print("Erro Super Solda:", e)
ui.pb.setValue(0); ui.pb.setFormat("Pronto") ui.pb.setValue(0); ui.pb.setFormat("Pronto")
def optimize_geometry(ui): def optimize_geometry(ui):
mprint("======== [VR4Life] GATILHO DA INTERFACE: P3 [Optimize] ========")
sp = ui.spn_pct.value() sp = ui.spn_pct.value()
tg = ui.spn_min_poly.value() tg = ui.spn_min_poly.value()
tgs = ui.get_processable_items() tgs = ui.get_processable_items()
@ -189,312 +267,391 @@ def optimize_geometry(ui):
ui.pb.setValue(tot) ui.pb.setValue(tot)
ui.pb.setFormat("Opt Concluída!") ui.pb.setFormat("Opt Concluída!")
def slice_large_objects(ui, from_auto=False):
if not ui.bake_items: return def prepare_mesh_v19(ui):
thr = ui.spn_max_sz.value() mprint("======== [VR4Life] GATILHO DA INTERFACE: P5 [Prepare UVP5] ========")
tgs = ui.get_processable_items() tgs = ui.get_processable_items()
tot = len(tgs) tot = len(tgs)
ui.pb.setMaximum(tot) ui.pb.setMaximum(tot)
ui.pb.setValue(0) ui.pb.setValue(0)
rt.execute("max modify mode")
print("\n" + "="*50)
print("🔍 INICIANDO LOG: MULTI-UV (P4)")
print(f"-> Tamanho de corte (Threshold): {thr}")
print("="*50)
for i, d in enumerate(tgs): for i, d in enumerate(tgs):
if ui._is_cancelled: break if ui._is_cancelled: break
ui.pb.setFormat(f"Multi-UV ({i+1}/{tot}): {d['name']}...") ui.pb.setFormat(f"UV v19 ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i) ui.pb.setValue(i)
QtWidgets.QApplication.processEvents() QtWidgets.QApplication.processEvents()
print(f"\n--- Processando objeto: {d['name']} ---") o = rt.getNodeByName(d['name'])
if o:
# CÓDIGO CORRIGIDO: Tudo embrulhado num "fn" (função) nativo para o return funcionar. try:
ms = f"""( ms = f"""(
fn applyMultiMatID objName thrVal = ( local obj = getNodeByName "{d['name']}"
local o = getNodeByName objName if obj != undefined do (
if o == undefined do return -1 max modify mode
if not isKindOf o Editable_Poly do convertToPoly o select obj
local theMod = Unwrap_UVW()
local dx = abs(o.max.x - o.min.x) modPanel.addModToSelection theMod ui:on
local dy = abs(o.max.y - o.min.y)
local dz = abs(o.max.z - o.min.z) -- Usando Channel 2 para o Bake (Corrigido o bug que gerava Channel 3 e deixava as UVs dessincronizadas do motor render)
local md = amax #(dx, dy, dz) theMod.setMapChannel 2
if md <= thrVal do ( -- Lógica original do V19 reconstruída para "Pack Custom" real
for f = 1 to (polyop.getNumFaces o) do polyop.setFaceMatID o f 1 theMod.flattenMap 45.0 #() 0.01 true 0 true true
return 1 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}")
local numChunks = (ceil (md / thrVal)) as integer ui.pb.setValue(tot)
if numChunks > 10 do numChunks = 10 ui.pb.setFormat("UV Pack V19 Concluído!")
local axis = if md == dx then 1 else if md == dy then 2 else 3
local minVal = if axis == 1 then o.min.x else if axis == 2 then o.min.y else o.min.z
local step = md / numChunks
for f = 1 to (polyop.getNumFaces o) do ( def close_annoying_windows():
local center = polyop.getFaceCenter o f ms = """(
local val = if axis == 1 then center.x else if axis == 2 then center.y else center.z try (
local id = (floor ((val - minVal) / step)) as integer + 1 local desktopHWND = windows.getDesktopHWND()
if id > numChunks do id = numChunks local children = windows.getChildrenHWND desktopHWND
if id < 1 do id = 1 local targets = #("Corona", "V-Ray", "Buffer", "Render", "Warning", "Error")
polyop.setFaceMatID o f id
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 ()
) )
return numChunks
) )
try ( applyMultiMatID "{d['name']}" {thr} ) catch ( -2 ) )
) catch ()
)""" )"""
rt.execute(ms)
try: def inspect_uv(ui):
print("-> Executando MaxScript de Fatiamento Lógico...")
r = rt.execute(ms)
print(f"-> Resposta do MaxScript: {r}")
if r == -1:
print("🚨 ERRO: Objeto não encontrado na cena pelo MaxScript.")
d['item'].setText(1, "Erro: Objeto")
elif r == -2:
print("🚨 ERRO: MaxScript falhou internamente (Topologia corrompida?).")
d['item'].setText(1, "Erro: Script")
elif r and int(r) > 1:
print(f"✅ SUCESSO: Objeto dividido em {int(r)} IDs!")
d['item'].setText(1, f"IDs: {int(r)}")
else:
print("-> Objeto menor que a Meta. Mantido como 1 ID.")
d['item'].setText(1, "1 ID (Normal)")
except Exception as e:
d['item'].setText(1, "Erro Python")
print(f"🚨 ERRO CRÍTICO PYTHON no Multi-UV: {e}")
ui.pb.setValue(tot)
ui.pb.setFormat("Multi-IDs Gerados!")
print("\n" + "="*50)
print("✅ FIM DO LOG MULTI-UV")
print("="*50 + "\n")
def prepare_mesh(ui):
tgs = ui.get_processable_items() tgs = ui.get_processable_items()
tot = len(tgs) if len(tgs) != 1:
ui.pb.setMaximum(tot) QtWidgets.QMessageBox.warning(ui, "Aviso", "Selecione exatamente UM objeto na lista para auditar a UV.")
ui.pb.setValue(0) return
rt.execute("max modify mode")
for i, d in enumerate(tgs): obj_name = tgs[0]['name']
if ui._is_cancelled: break
ui.pb.setFormat(f"UV ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i)
QtWidgets.QApplication.processEvents()
o = rt.getNodeByName(d['name'])
if o:
rt.select(o)
try:
ms = f"""( ms = f"""(
local o = getNodeByName "{d['name']}" local o = getNodeByName "{obj_name}"
if not isKindOf o Editable_Poly do convertToPoly o if o != undefined and isValidNode o do (
local m = Unwrap_UVW() select o
addModifier o m max modify mode
m.setMapChannel 3 if o.modifiers[#Unwrap_UVW] != undefined then (
m.setTVSubObjectMode 3 modPanel.setCurrentObject o.modifiers[#Unwrap_UVW]
local maxID = 1 o.modifiers[#Unwrap_UVW].edit()
for f = 1 to (polyop.getNumFaces o) do ( ) else (
local id = polyop.getFaceMatID o f messageBox "O modificador Unwrap_UVW nao foi encontrado."
if id > maxID do maxID = id )
) )
for x = 1 to maxID do (
m.selectByMatID x
local sel = m.getSelectedFaces()
if not sel.isEmpty do (
m.flattenMap 45.0 #() 0.002 true 0 true false
m.pack 1 0.002 true false false
)
)
collapseStack o
)""" )"""
rt.execute(ms) rt.execute(ms)
if not rt.isKindOf(o, rt.Editable_Poly):
rt.convertTo(o, rt.Editable_Poly)
d['item'].setText(1, "UV Packed (C3)")
except:
d['item'].setText(1, "Erro UV")
ui.pb.setValue(tot)
ui.pb.setFormat("UV Pack Concluído!")
def process_bake_logic(ui, auto_export=False): 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 ui._is_cancelled = False
el = ui.cmb_bake_elem.currentText()
p_bk = ui.edt_p_bake.text()
rnd = str(rt.execute("renderers.current as string"))
i_vr = "V_Ray" in rnd
i_cor = "Corona" in rnd
u_den = ui.chk_denoise.isChecked()
a_res = ui.chk_a_res.isChecked()
if not os.path.exists(p_bk): os.makedirs(p_bk)
tgs = ui.get_processable_items() tgs = ui.get_processable_items()
if not tgs: return
tot = len(tgs) tot = len(tgs)
ui.pb.setMaximum(tot) ui.pb.setMaximum(tot)
ui.pb.setValue(0) ui.pb.setValue(0)
print("\n" + "="*50) # Extract UI variables once
print("🎬 INICIANDO LOG: MOTOR DE BAKE (P6)") p_bk = ui.edt_p_bake.text().replace("\\", "/")
print(f"-> Pasta Alvo: {p_bk}") if not p_bk.endswith("/"): p_bk += "/"
print(f"-> Renderizador: {rnd}") if not os.path.exists(p_bk): os.makedirs(p_bk)
print("="*50)
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): for i, d in enumerate(tgs):
if ui._is_cancelled: break if ui._is_cancelled:
print(f"\n🚨 Bake Cancelado pelo usuario.")
break
ui.pb.setFormat(f"Bake ({i+1}/{tot}): {d['name']}...") ui.pb.setFormat(f"Bake ({i+1}/{tot}): {d['name']}...")
ui.pb.setValue(i) ui.pb.setValue(i)
QtWidgets.QApplication.processEvents() QtWidgets.QApplication.processEvents()
print(f"\n--- Preparando Bake: {d['name']} ---") 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
)
)
o = rt.getNodeByName(d['name']) -------------------------------------------------------------------------
if not o: -- 2. PREPARE BAKE ELEMENTS
print("🚨 ERRO: Objeto não encontrado.") -------------------------------------------------------------------------
continue obj.INodeBakeProperties.removeAllBakeElements()
local be = undefined
local fileExt = ".jpg"
try: local curRen = renderers.current
# 1. Conta quantos IDs existem no objeto local isCorona = matchPattern (curRen as string) pattern:"*Corona*"
ms_mid = f"""(local o = getNodeByName "{d['name']}"; local mid = 1; if o != undefined and isKindOf o Editable_Poly do ( for f = 1 to (polyop.getNumFaces o) do ( local id = polyop.getFaceMatID o f; if id > mid do mid = id ) ); mid)""" local isVRay = matchPattern (curRen as string) pattern:"*V_Ray*"
max_id = rt.execute(ms_mid)
if not max_id: max_id = 1
max_id = int(max_id)
print(f"-> Total de IDs a renderizar: {max_id}")
rt.execute("max select none")
rt.select(o)
rt.execute("max modify mode")
rt.execute("max zoomext sel all")
rt.redrawViews()
time.sleep(0.5)
for mid in range(1, max_id + 1): if isVRay then ( try ( be = VRayCompleteMap() ) catch ( be = CompleteMap() ) )
if ui._is_cancelled: break else if isCorona then ( try ( be = Corona_Beauty() ) catch ( try ( be = CShading_Beauty() ) catch ( be = CompleteMap() ) ) )
print(f"\n-> Assando ID: {mid} de {max_id}") else ( be = CompleteMap() )
if max_id == 1: if be == undefined then be = CompleteMap()
t_jpg_id = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/")
else:
t_jpg_id = os.path.join(p_bk, f"{d['name']}_B_ID{mid}.jpg").replace("\\", "/")
if os.path.exists(t_jpg_id): be.outputSzX = {sz}
d['item'].setText(1, f"Já existe ID{mid}") be.outputSzY = {sz}
print("-> Ficheiro já existe. Pulando.") be.fileType = fileExt
continue
cr = ui.spn_res.value() local pBk = @"{p_bk}"
if a_res: be.filename = (pBk + obj.name + "_Baked" + fileExt)
md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z)) try ( be.fileOut = be.filename ) catch()
cr = 256 if md <= ui.s256.value() else 512 if md <= ui.s512.value() else 1024 if md <= ui.s1024.value() else cr
local fileExists = doesFileExist be.filename
ext = ".exr" if (i_vr and u_den) else ".jpg" local skipRender = false -- Overwrite always enabled via UI integration
t_rnd_id = t_jpg_id.replace(".jpg", ".exr") if ext == ".exr" else t_jpg_id
if skipRender then (
if max_id > 1: print "Skipped (Exists)"
print("-> Isolando UV do ID atual...") ) else (
# CÓDIGO CORRIGIDO 1: Embrulhado em 'fn' obj.INodeBakeProperties.addBakeElement be
ms_uv = f"""( obj.INodeBakeProperties.bakeEnabled = true
fn isolateUV currentMid = ( obj.INodeBakeProperties.bakeChannel = 2
global temp_uv_mod = Unwrap_UVW()
addModifier $ temp_uv_mod if isCorona and ({cv_native} == false) then (
temp_uv_mod.setMapChannel 3 try(renderers.current.pass_limit = {cv_passes}; renderers.current.time_limit = 0; renderers.current.noise_level_limit = 0.0)catch()
temp_uv_mod.setTVSubObjectMode 3
local allF = #{{1..(polyop.getNumFaces $)}}
temp_uv_mod.selectByMatID currentMid
local tgtF = temp_uv_mod.getSelectedFaces()
local hideF = allF - tgtF
if hideF.numberset > 0 do (
temp_uv_mod.selectFaces hideF
temp_uv_mod.moveSelected [-10, -10, 0]
) )
return 1
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()
) )
try ( isolateUV {mid} ) catch ( 0 )
)"""
res_uv = rt.execute(ms_uv)
if res_uv == 0: print("🚨 ERRO: O MaxScript não conseguiu manipular as UVs.")
print(f"-> Disparando o Render: {t_rnd_id}")
# CÓDIGO CORRIGIDO 2: Embrulhado em 'fn'
ms_bake = f"""(
fn doBake fileOut szX szY fileExt cor den elem = (
try(freeSceneBitmaps(); vfbControl #clearimage)catch()
if cor and den do try ( renderers.current.denoise_enable = true ) catch()
$.INodeBakeProperties.removeAllBakeElements()
local be = (execute (elem + "()"))
be.outputSzX = szX
be.outputSzY = szY
be.fileType = fileExt
be.fileName = fileOut
$.INodeBakeProperties.addBakeElement be print "Disparando Render API V19..."
$.INodeBakeProperties.bakeEnabled = true -- Fix 2024: explicit outputfile to guarantee disk writing and prevent black/missing files
$.INodeBakeProperties.bakeChannel = 3 render rendertype:#bakeSelected vfb:true progressBar:true quiet:true outputSize:[{sz}, {sz}] outputfile:be.filename cancelled:&wasCancelled
render rendertype:#bakeSelected vfb:true quiet:true production:true outputfile:fileOut ) catch (
return 1 print "Render Error/Cancel Caught."
wasCancelled = true
) )
try ( doBake "{t_rnd_id}" {cr} {cr} "{ext}" {str(i_cor).lower()} {str(u_den).lower()} "{el}" ) catch ( 0 )
)"""
res_bk = rt.execute(ms_bake)
if res_bk == 0: print("🚨 ERRO: O Renderizador rejeitou o comando de Bake!")
wt = 0 print "Desativando Silenciador de Popups..."
while not os.path.exists(t_rnd_id) and wt < 60: DialogMonitorOPS.enabled = false
time.sleep(0.5) DialogMonitorOPS.unRegisterNotification id:#vr4_kill
wt += 0.5
QtWidgets.QApplication.processEvents()
if os.path.exists(t_rnd_id): if wasCancelled then return -1
if i_vr and u_den: )
print("-> Passando Denoise IA...")
c_in = t_rnd_id.replace("/", "\\")
c_out = t_rnd_id.replace(".exr", "_denoised.exr").replace("/", "\\")
try:
p = subprocess.Popen(f'{get_vdenoise_path()} -inputFile="{c_in}" -outputFile="{c_out}" -display=0', shell=True, creationflags=0x08000000); dt = 0
while p.poll() is None and dt < 60: QtWidgets.QApplication.processEvents(); time.sleep(0.5); dt += 0.5
if p.poll() is None: p.terminate()
except: pass
f_exr = c_out.replace("\\", "/") if os.path.exists(c_out.replace("\\", "/")) else t_rnd_id
rt.execute(f"""( try(vfbControl #clearimage)catch(); try ( local bI = openBitmap @"{f_exr}"; if bI != undefined do ( local bO = bitmap bI.width bI.height filename:@"{t_jpg_id}" hdr:true; copy bI bO; save bO; close bI; close bO; free bI; free bO ) ) catch() )""")
rt.execute("freeSceneBitmaps(); gc light:true"); time.sleep(0.5)
try: os.remove(t_rnd_id); os.remove(c_out.replace("\\", "/"))
except: pass
if max_id > 1: -------------------------------------------------------------------------
print("-> Removendo modificador temporário de UV...") -- 4. MATERIAL CREATION
rt.execute("try(deleteModifier $ globalvars.get #temp_uv_mod)catch()") -------------------------------------------------------------------------
local newMat = PhysicalMaterial()
newMat.name = (obj.name + "_Baked_Mat")
newMat.base_color = color 255 255 255
newMat.roughness = 1.0
newMat.emission = 0.0
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 os.path.exists(t_jpg_id): if fileFound then (
d['item'].setText(1, "Bake OK") local bmpTex = BitmapTexture filename:be.filename
print("-> SUCESSO: Textura gravada no disco.") if {tgt_uv_state} == 2 then bmpTex.coords.mapChannel = 2 else bmpTex.coords.mapChannel = 1
else: newMat.base_color_map = bmpTex
d['item'].setText(1, "Erro Conv") showTextureMap newMat newMat.base_color_map on
print("🚨 ERRO: A textura não foi salva. Timeout?") ) else (
print ("ERROR: Texture file not found: " + be.filename)
return -2
)
if not ui._is_cancelled: -------------------------------------------------------------------------
print("-> Restaurando UV final do objeto...") -- 5. FINAL CLEANUP
rt.execute(f"""( local o = getNodeByName "{d['name']}"; try( ChannelInfo.CopyChannel o 3 3; ChannelInfo.PasteChannel o 3 1; collapseStack o )catch() )""") -------------------------------------------------------------------------
sleep 1.0
obj.material = newMat
except Exception as e: if {tgt_uv_state} == 1 then (
d['item'].setText(1, "Erro Render") channelInfo.CopyChannel obj 3 2
print(f"🚨 EXCEÇÃO PYTHON: {e}") channelInfo.PasteChannel obj 3 1
channelInfo.ClearChannel obj 2
)
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.setValue(tot)
ui.pb.setFormat("Bake Concluído!") ui.pb.setFormat("Bake V19 Concluído!")
print("\n" + "="*50) print("\n" + "="*50)
print("✅ FIM DO LOG DE BAKE") print("✅ FIM DO LOG DE BAKE V19")
print("="*50 + "\n") print("="*50 + "\n")
if auto_export and not ui._is_cancelled: if auto_export and not ui._is_cancelled:
cld.finalize_export(ui, p_bk, ui.edt_p_glb.text()) 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
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
-- APENAS CLONAR PARA EXPORTACAO, SEM MANIPULAR UV OU MATERIAIS
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): def upd_res_col(ui):
a_r = ui.chk_a_res.isChecked() a_r = ui.chk_a_res.isChecked()

@ -1,4 +1,5 @@
import os import os
import json
from pymxs import runtime as rt from pymxs import runtime as rt
try: from PySide6 import QtWidgets, QtCore, QtGui try: from PySide6 import QtWidgets, QtCore, QtGui
except ImportError: from PySide2 import QtWidgets, QtCore, QtGui except ImportError: from PySide2 import QtWidgets, QtCore, QtGui
@ -20,15 +21,17 @@ def get_max_window_safe():
class AutoBakeManager(QtWidgets.QDialog): class AutoBakeManager(QtWidgets.QDialog):
def __init__(self): def __init__(self):
super(AutoBakeManager, self).__init__(get_max_window_safe()) super(AutoBakeManager, self).__init__(get_max_window_safe())
self.setWindowTitle("VR4LIFE AUTO-BAKE V167.0 - MODULAR ENTERPRISE") self.setWindowTitle("VR4LIFE AUTO-BAKE V312 - MODULAR ENTERPRISE")
self.resize(1050, 1380); self.bake_items = []; self._is_cancelled = False self.setWindowFlags(self.windowFlags() | QtCore.Qt.WindowMinimizeButtonHint | QtCore.Qt.WindowMaximizeButtonHint | QtCore.Qt.WindowCloseButtonHint)
self.resize(570, 890); self.bake_items = []; self._is_cancelled = False
p = self.palette(); p.setColor(QtGui.QPalette.Window, QtGui.QColor(43, 43, 43)); self.setPalette(p); self.setAutoFillBackground(True) p = self.palette(); p.setColor(QtGui.QPalette.Window, QtGui.QColor(43, 43, 43)); self.setPalette(p); self.setAutoFillBackground(True)
self.init_ui() self.init_ui()
rt.clearListener(); eng.load_bake_elements(self) eng.load_bake_elements(self)
def init_ui(self): def init_ui(self):
layout = QtWidgets.QVBoxLayout(self) layout = QtWidgets.QVBoxLayout(self)
lbl_t = QtWidgets.QLabel("VR4LIFE - V167.0 (Modular)"); lbl_t.setAlignment(QtCore.Qt.AlignCenter); lbl_t.setStyleSheet("font-size: 24px; font-weight: bold; color: #00FF00;") # --- TOP HEADER ---
lbl_t = QtWidgets.QLabel("VR4LIFE - V312 (Modular) ( ultima alteração 2:56 AM 11-03- 2026)"); lbl_t.setAlignment(QtCore.Qt.AlignCenter); lbl_t.setStyleSheet("font-size: 24px; font-weight: bold; color: #00FF00;")
layout.addWidget(lbl_t) layout.addWidget(lbl_t)
self.tabs = QtWidgets.QTabWidget() self.tabs = QtWidgets.QTabWidget()
@ -39,11 +42,18 @@ class AutoBakeManager(QtWidgets.QDialog):
self.btn_ref = QtWidgets.QPushButton("🔄 RECARREGAR MAPAS"); self.btn_ref.setStyleSheet("background: #555; color: white; height: 35px;"); self.btn_ref.clicked.connect(lambda: eng.load_bake_elements(self)) self.btn_ref = QtWidgets.QPushButton("🔄 RECARREGAR MAPAS"); self.btn_ref.setStyleSheet("background: #555; color: white; height: 35px;"); self.btn_ref.clicked.connect(lambda: eng.load_bake_elements(self))
self.cmb_bake_elem = QtWidgets.QComboBox(); self.cmb_bake_elem.setStyleSheet("background: white; color: black; font-weight: bold;") self.cmb_bake_elem = QtWidgets.QComboBox(); self.cmb_bake_elem.setStyleSheet("background: white; color: black; font-weight: bold;")
self.chk_denoise = QtWidgets.QCheckBox("✨ Ativar Denoiser IA"); self.chk_denoise.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_denoise.setChecked(True) self.chk_denoise = QtWidgets.QCheckBox("✨ Ativar Denoiser IA"); self.chk_denoise.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_denoise.setChecked(True)
self.spn_light_boost = QtWidgets.QSpinBox(); self.spn_light_boost.setStyleSheet("background: white; color: black; font-weight: bold;"); self.spn_light_boost.setRange(0, 50); self.spn_light_boost.setSuffix("%"); self.spn_light_boost.setToolTip("Aumenta temporariamente a intensidade de todas as luzes da cena para o Bake.")
self.edt_p_bake = QtWidgets.QLineEdit(rt.maxFilePath + "_BAKE_JPG\\" if rt.maxFilePath else "C:\\VR4_BAKE\\"); self.edt_p_bake.setStyleSheet("background: white; color: black;") self.edt_p_bake = QtWidgets.QLineEdit(rt.maxFilePath + "_BAKE_JPG\\" if rt.maxFilePath else "C:\\VR4_BAKE\\"); self.edt_p_bake.setStyleSheet("background: white; color: black;")
self.edt_p_glb = QtWidgets.QLineEdit(rt.maxFilePath + "_EXPORT_GLB\\" if rt.maxFilePath else "C:\\VR4_GLB\\"); self.edt_p_glb.setStyleSheet("background: white; color: black;") self.edt_p_glb = QtWidgets.QLineEdit(rt.maxFilePath + "_EXPORT_GLB\\" if rt.maxFilePath else "C:\\VR4_GLB\\"); self.edt_p_glb.setStyleSheet("background: white; color: black;")
self.btn_brw_glb = QtWidgets.QPushButton("..."); self.btn_brw_glb.setStyleSheet("background: #777; color: white; font-weight: bold;"); self.btn_brw_glb.clicked.connect(self.browse_glb_folder) self.btn_brw_glb = QtWidgets.QPushButton("..."); self.btn_brw_glb.setStyleSheet("background: #777; color: white; font-weight: bold;"); self.btn_brw_glb.clicked.connect(self.browse_glb_folder)
h_glb = QtWidgets.QHBoxLayout(); h_glb.addWidget(self.edt_p_glb); h_glb.addWidget(self.btn_brw_glb) h_glb = QtWidgets.QHBoxLayout(); h_glb.addWidget(self.edt_p_glb); h_glb.addWidget(self.btn_brw_glb)
f_gen.addRow("Mapa:", self.cmb_bake_elem); f_gen.addRow("", self.chk_denoise); f_gen.addRow("Pasta JPG:", self.edt_p_bake); f_gen.addRow("Pasta GLB/ZIP:", h_glb) self.rdo_uv1 = QtWidgets.QRadioButton("UV 1 (Substituir)"); self.rdo_uv1.setStyleSheet("color: white; font-weight: bold;")
self.rdo_uv2 = QtWidgets.QRadioButton("UV 2 (Multi-UV)"); self.rdo_uv2.setStyleSheet("color: white; font-weight: bold;"); self.rdo_uv2.setChecked(True)
h_uv = QtWidgets.QHBoxLayout(); h_uv.addWidget(self.rdo_uv1); h_uv.addWidget(self.rdo_uv2)
f_gen.addRow("Mapa:", self.cmb_bake_elem); f_gen.addRow("", self.chk_denoise); f_gen.addRow("Boost de Luz:", self.spn_light_boost)
f_gen.addRow("Destino UV:", h_uv)
f_gen.addRow("Pasta JPG:", self.edt_p_bake); f_gen.addRow("Pasta GLB/ZIP:", h_glb)
l_gen.addWidget(self.btn_ref); l_gen.addLayout(f_gen); l_gen.addStretch(); self.tabs.addTab(t_gen, "🗂️ 1. Geral") l_gen.addWidget(self.btn_ref); l_gen.addLayout(f_gen); l_gen.addStretch(); self.tabs.addTab(t_gen, "🗂️ 1. Geral")
# ABA 2 # ABA 2
@ -68,6 +78,13 @@ class AutoBakeManager(QtWidgets.QDialog):
h_sz = QtWidgets.QHBoxLayout(); h_sz.addWidget(self.chk_a_res); h_sz.addWidget(self.s256); h_sz.addWidget(self.s512); h_sz.addWidget(self.s1024) h_sz = QtWidgets.QHBoxLayout(); h_sz.addWidget(self.chk_a_res); h_sz.addWidget(self.s256); h_sz.addWidget(self.s512); h_sz.addWidget(self.s1024)
for w in [self.chk_a_res, self.s256, self.s512, self.s1024, self.spn_res]: w.valueChanged.connect(self.upd_res_col) if hasattr(w, 'valueChanged') else w.toggled.connect(self.upd_res_col) for w in [self.chk_a_res, self.s256, self.s512, self.s1024, self.spn_res]: w.valueChanged.connect(self.upd_res_col) if hasattr(w, 'valueChanged') else w.toggled.connect(self.upd_res_col)
f_tex.addRow("Res Máx (>):", self.spn_res); f_tex.addRow("Escala:", h_sz) f_tex.addRow("Res Máx (>):", self.spn_res); f_tex.addRow("Escala:", h_sz)
self.spn_passes = QtWidgets.QSpinBox(); self.spn_passes.setStyleSheet("background: white; color: black;"); self.spn_passes.setRange(1, 9999); self.spn_passes.setValue(5)
self.chk_native = QtWidgets.QCheckBox("Usar F10 Nativ."); self.chk_native.setStyleSheet("color: white; font-weight: bold;"); self.chk_native.setChecked(False)
self.btn_f10 = QtWidgets.QPushButton("⚙️ F10 Config"); self.btn_f10.setStyleSheet("background: #555; color: white;"); self.btn_f10.clicked.connect(lambda: rt.execute("max render options"))
h_cor = QtWidgets.QHBoxLayout(); h_cor.addWidget(self.spn_passes); h_cor.addWidget(self.chk_native); h_cor.addWidget(self.btn_f10)
f_tex.addRow("Corona Limit (Pass):", h_cor)
l_tex.addLayout(f_tex); l_tex.addStretch(); self.tabs.addTab(t_tex, "🎨 3. Textura") l_tex.addLayout(f_tex); l_tex.addStretch(); self.tabs.addTab(t_tex, "🎨 3. Textura")
t_cld = QtWidgets.QWidget(); l_cld = QtWidgets.QVBoxLayout(t_cld); f_cld = QtWidgets.QFormLayout() t_cld = QtWidgets.QWidget(); l_cld = QtWidgets.QVBoxLayout(t_cld); f_cld = QtWidgets.QFormLayout()
@ -93,27 +110,58 @@ class AutoBakeManager(QtWidgets.QDialog):
self.tree.setColumnWidth(0, 260); self.tree.setColumnWidth(1, 140); self.tree.setColumnWidth(2, 70); self.tree.setColumnWidth(3, 70); self.tree.setColumnWidth(4, 70); self.tree.itemClicked.connect(self.on_item_click) self.tree.setColumnWidth(0, 260); self.tree.setColumnWidth(1, 140); self.tree.setColumnWidth(2, 70); self.tree.setColumnWidth(3, 70); self.tree.setColumnWidth(4, 70); self.tree.itemClicked.connect(self.on_item_click)
layout.addWidget(self.tree) layout.addWidget(self.tree)
# ====== RESTAURAR ESTADO APÓS RECARREGAR (PERSISTENTE NA CENA) ======
rt.execute("persistent global VR4Life_SceneListState")
state_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vr4_state.json")
saved_items = []
try:
# 1. Tenta recuperar da memória da cena atual
state_str = rt.VR4Life_SceneListState
if state_str and isinstance(state_str, str):
saved_items = json.loads(state_str)
# 2. Fallback pro arquivo local antigo
elif os.path.exists(state_file):
with open(state_file, 'r', encoding='utf-8') as f:
saved_items = json.load(f)
os.remove(state_file)
for d in saved_items:
it = QtWidgets.QTreeWidgetItem([d.get('name', ''), d.get('status', ''), d.get('polys', ''), d.get('res', ''), d.get('id', '')])
self.tree.addTopLevelItem(it)
self.bake_items.append({'name': d.get('name', ''), 'item': it})
except Exception as e:
print("Aviso: Falha ao restaurar estado da UI da Cena:", e)
self.pb = QtWidgets.QProgressBar(); self.pb.setAlignment(QtCore.Qt.AlignCenter); self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: black; background: white; } QProgressBar::chunk { background: #00FF00; }"); self.pb.setFormat("Pronto") self.pb = QtWidgets.QProgressBar(); self.pb.setAlignment(QtCore.Qt.AlignCenter); self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: black; background: white; } QProgressBar::chunk { background: #00FF00; }"); self.pb.setFormat("Pronto")
layout.addWidget(self.pb) layout.addWidget(self.pb)
h_l = QtWidgets.QHBoxLayout() h_l = QtWidgets.QHBoxLayout()
self.b1 = QtWidgets.QPushButton("P1: Lista"); self.b1.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b1.clicked.connect(lambda: eng.load_selection(self)) self.b1 = QtWidgets.QPushButton("P1: Lista"); self.b1.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b1.clicked.connect(lambda: eng.load_selection(self))
self.b2 = QtWidgets.QPushButton("P2: Fundir"); self.b2.setStyleSheet("background: #32CD32; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2.clicked.connect(lambda: eng.attach_grouped_objects(self, False)) # self.b2 = QtWidgets.QPushButton("P2: Fundir"); self.b2.setStyleSheet("background: #32CD32; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2.clicked.connect(lambda: eng.attach_grouped_objects(self, False))
self.b2_5 = QtWidgets.QPushButton("P2.5: Super Solda"); self.b2_5.setStyleSheet("background: #008080; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2_5.clicked.connect(lambda: eng.super_attach_objects(self, False)) # self.b2_5 = QtWidgets.QPushButton("P2.5: Super Solda"); self.b2_5.setStyleSheet("background: #008080; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2_5.clicked.connect(lambda: eng.super_attach_objects(self, False))
self.b3 = QtWidgets.QPushButton("P3: Opt"); self.b3.setStyleSheet("background: #FF8C00; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b3.clicked.connect(lambda: eng.optimize_geometry(self))
self.b4 = QtWidgets.QPushButton("P4: Multi-UV"); self.b4.setStyleSheet("background: #8B008B; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b4.clicked.connect(lambda: eng.slice_large_objects(self, False)) self.b3 = QtWidgets.QPushButton("P3: Opt"); self.b3.setStyleSheet("background: #FF8C00; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b3.clicked.connect(lambda: eng.optimize_geometry(self))
self.b5 = QtWidgets.QPushButton("P5: UV"); self.b5.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b5.clicked.connect(lambda: eng.prepare_mesh(self)) self.b5 = QtWidgets.QPushButton("P5: UV"); self.b5.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b5.clicked.connect(lambda: eng.prepare_mesh_v19(self))
self.b6 = QtWidgets.QPushButton("P6: Bake"); self.b6.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b6.clicked.connect(lambda: eng.process_bake_logic(self, False)) self.b6 = QtWidgets.QPushButton("P6: Bake"); self.b6.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b6.clicked.connect(lambda: eng.process_bake_logic_v19(self, False))
self.b7 = QtWidgets.QPushButton("P7: Exportar"); self.b7.setStyleSheet("background: #FFD700; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b7.clicked.connect(lambda: cld.finalize_export(self, self.edt_p_bake.text(), self.edt_p_glb.text())) self.b7 = QtWidgets.QPushButton("P7: Exportar"); self.b7.setStyleSheet("background: #FFD700; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b7.clicked.connect(lambda: eng.export_glb_v19(self))
self.b8 = QtWidgets.QPushButton("P8: Inspec."); self.b8.setStyleSheet("background: #9370DB; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b8.clicked.connect(lambda: eng.inspect_uv(self))
self.b_clear = QtWidgets.QPushButton("🗑️ Limpar"); self.b_clear.setStyleSheet("background: #666; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b_clear.clicked.connect(self.clear_list)
for b in [self.b1, self.b2, self.b2_5, self.b3, self.b4, self.b5, self.b6, self.b7]: h_l.addWidget(b) for b in [self.b1, self.b3, self.b5, self.b6, self.b7, self.b8, self.b_clear]: h_l.addWidget(b)
layout.addLayout(h_l) layout.addLayout(h_l)
self.btn_cancel = QtWidgets.QPushButton("CANCELAR / FECHAR"); self.btn_cancel.setStyleSheet("background: #FF0000; color: white; font-weight: bold; height: 40px; border-radius: 8px;"); self.btn_cancel.clicked.connect(self.cancel_all) self.btn_cancel = QtWidgets.QPushButton("CANCELAR OPERAÇÃO"); self.btn_cancel.setStyleSheet("background: #FF0000; color: white; font-weight: bold; height: 40px; border-radius: 8px; margin-top: 10px;"); self.btn_cancel.clicked.connect(self.cancel_all)
layout.addWidget(self.btn_cancel) layout.addWidget(self.btn_cancel)
def clear_list(self):
self.tree.clear()
self.bake_items.clear()
self.pb.setFormat("Lista Vazia")
self.pb.setValue(0)
def browse_glb_folder(self): def browse_glb_folder(self):
f = QtWidgets.QFileDialog.getExistingDirectory(self, "Selecione a Pasta GLB") f = QtWidgets.QFileDialog.getExistingDirectory(self, "Selecione a Pasta GLB")
if f: self.edt_p_glb.setText(f + "\\") if f: self.edt_p_glb.setText(f + "\\")
@ -132,13 +180,53 @@ class AutoBakeManager(QtWidgets.QDialog):
def run_full_auto(self): def run_full_auto(self):
self._is_cancelled = False self._is_cancelled = False
if not rt.execute("selection as array"): QtWidgets.QMessageBox.warning(self, "Aviso", "Selecione algo!"); return if not rt.execute("selection as array"): QtWidgets.QMessageBox.warning(self, "Aviso", "Selecione algo!"); return
if self.chk_a_weld.isChecked() and not self._is_cancelled: eng.attach_grouped_objects(self, True) # if self.chk_a_weld.isChecked() and not self._is_cancelled: eng.attach_grouped_objects(self, True)
if self.chk_a_super.isChecked() and not self._is_cancelled: eng.super_attach_objects(self, True) # if self.chk_a_super.isChecked() and not self._is_cancelled: eng.super_attach_objects(self, True)
eng.load_selection(self) eng.load_selection(self)
if not self.get_processable_items(): return if not self.get_processable_items(): return
if self.bake_items and not self._is_cancelled: eng.optimize_geometry(self) if self.bake_items and not self._is_cancelled: eng.optimize_geometry(self)
if self.chk_a_slice.isChecked() and not self._is_cancelled: eng.slice_large_objects(self, True) if not self._is_cancelled: eng.prepare_mesh_v19(self)
if not self._is_cancelled: eng.prepare_mesh(self)
if not self._is_cancelled: time.sleep(1.0); eng.process_bake_logic(self, True) def save_persistent_state(self):
state_data = []
for d in self.bake_items:
it = d['item']
state_data.append({
'name': d['name'],
'status': it.text(1),
'polys': it.text(2),
'res': it.text(3),
'id': it.text(4)
})
try:
# Salva na Cena do 3ds Max!
json_str = json.dumps(state_data)
rt.execute("persistent global VR4Life_SceneListState")
rt.VR4Life_SceneListState = json_str
# Fallback local (pra garantir)
state_file = os.path.join(os.path.dirname(os.path.realpath(__file__)), "vr4_state.json")
with open(state_file, 'w', encoding='utf-8') as f:
json.dump(state_data, f)
except Exception as e:
print("Erro ao salvar o estado da UI na cena:", e)
def closeEvent(self, event):
self.save_persistent_state()
try: super().closeEvent(event)
except: pass
def reload_plugin(self):
self._is_cancelled = True
self.save_persistent_state()
self.close()
p = os.path.join(os.path.dirname(os.path.realpath(__file__)), "run_vr4life.py")
rt.execute(f"python.ExecuteFile @\"{p}\"")
def cancel_all(self): self._is_cancelled = True; self.close() def cancel_all(self):
self._is_cancelled = True
self.pb.setFormat("⚠️ OPERAÇÃO CANCELADA PELO USUÁRIO")
self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: white; background: #FF0000; }")
Loading…
Cancel
Save