From 5e779cf9ec8b3095104f1e51a8c726b9977005f5 Mon Sep 17 00:00:00 2001 From: PauloHNCosta Date: Wed, 11 Mar 2026 09:41:04 -0300 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20Release=20Notes=20-=20VR4Life=20?= =?UTF-8?q?Plugin=20v312?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 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). --- BKP_deSegurança/install_vr4life.py | 61 ++ BKP_deSegurança/run_vr4life.py | 15 + BKP_deSegurança/v19 vr4life.ms | 510 +++++++++++++++ BKP_deSegurança/version.txt | 0 BKP_deSegurança/vr4life.mnx | 17 + BKP_deSegurança/vr4life_cloud.py | 124 ++++ BKP_deSegurança/vr4life_engine.py | 521 ++++++++++++++++ BKP_deSegurança/vr4life_ui.py | 144 +++++ BKP_deSegurança/vr4life_updater.py | 92 +++ Install_VR4Life_Menu.ms | 45 ++ __pycache__/vr4life_cloud.cpython-310.pyc | Bin 0 -> 6691 bytes __pycache__/vr4life_engine.cpython-310.pyc | Bin 0 -> 27736 bytes __pycache__/vr4life_engine.cpython-313.pyc | Bin 0 -> 36354 bytes __pycache__/vr4life_ui.cpython-310.pyc | Bin 0 -> 13971 bytes run_vr4life.py | 2 + vr4_state.json | 1 + vr4life_engine.py | 681 +++++++++++++-------- vr4life_ui.py | 132 +++- 18 files changed, 2061 insertions(+), 284 deletions(-) create mode 100644 BKP_deSegurança/install_vr4life.py create mode 100644 BKP_deSegurança/run_vr4life.py create mode 100644 BKP_deSegurança/v19 vr4life.ms create mode 100644 BKP_deSegurança/version.txt create mode 100644 BKP_deSegurança/vr4life.mnx create mode 100644 BKP_deSegurança/vr4life_cloud.py create mode 100644 BKP_deSegurança/vr4life_engine.py create mode 100644 BKP_deSegurança/vr4life_ui.py create mode 100644 BKP_deSegurança/vr4life_updater.py create mode 100644 Install_VR4Life_Menu.ms create mode 100644 __pycache__/vr4life_cloud.cpython-310.pyc create mode 100644 __pycache__/vr4life_engine.cpython-310.pyc create mode 100644 __pycache__/vr4life_engine.cpython-313.pyc create mode 100644 __pycache__/vr4life_ui.cpython-310.pyc create mode 100644 vr4_state.json diff --git a/BKP_deSegurança/install_vr4life.py b/BKP_deSegurança/install_vr4life.py new file mode 100644 index 0000000..f8608ca --- /dev/null +++ b/BKP_deSegurança/install_vr4life.py @@ -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 já 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() \ No newline at end of file diff --git a/BKP_deSegurança/run_vr4life.py b/BKP_deSegurança/run_vr4life.py new file mode 100644 index 0000000..7f6f3bd --- /dev/null +++ b/BKP_deSegurança/run_vr4life.py @@ -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() \ No newline at end of file diff --git a/BKP_deSegurança/v19 vr4life.ms b/BKP_deSegurança/v19 vr4life.ms new file mode 100644 index 0000000..afc3508 --- /dev/null +++ b/BKP_deSegurança/v19 vr4life.ms @@ -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 +) diff --git a/BKP_deSegurança/version.txt b/BKP_deSegurança/version.txt new file mode 100644 index 0000000..e69de29 diff --git a/BKP_deSegurança/vr4life.mnx b/BKP_deSegurança/vr4life.mnx new file mode 100644 index 0000000..4c2aa8f --- /dev/null +++ b/BKP_deSegurança/vr4life.mnx @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/BKP_deSegurança/vr4life_cloud.py b/BKP_deSegurança/vr4life_cloud.py new file mode 100644 index 0000000..bae9d77 --- /dev/null +++ b/BKP_deSegurança/vr4life_cloud.py @@ -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) \ No newline at end of file diff --git a/BKP_deSegurança/vr4life_engine.py b/BKP_deSegurança/vr4life_engine.py new file mode 100644 index 0000000..f227c18 --- /dev/null +++ b/BKP_deSegurança/vr4life_engine.py @@ -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 \ No newline at end of file diff --git a/BKP_deSegurança/vr4life_ui.py b/BKP_deSegurança/vr4life_ui.py new file mode 100644 index 0000000..732f971 --- /dev/null +++ b/BKP_deSegurança/vr4life_ui.py @@ -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() \ No newline at end of file diff --git a/BKP_deSegurança/vr4life_updater.py b/BKP_deSegurança/vr4life_updater.py new file mode 100644 index 0000000..5b94abd --- /dev/null +++ b/BKP_deSegurança/vr4life_updater.py @@ -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") \ No newline at end of file diff --git a/Install_VR4Life_Menu.ms b/Install_VR4Life_Menu.ms new file mode 100644 index 0000000..a4615eb --- /dev/null +++ b/Install_VR4Life_Menu.ms @@ -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" +) diff --git a/__pycache__/vr4life_cloud.cpython-310.pyc b/__pycache__/vr4life_cloud.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2cf8a4129e53b4a1fdec9300ccfabf7d887de9b6 GIT binary patch literal 6691 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 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): +def prepare_mesh_v19(ui): + mprint("======== [VR4Life] GATILHO DA INTERFACE: P5 [Prepare UVP5] ========") tgs = ui.get_processable_items() tot = len(tgs) ui.pb.setMaximum(tot) ui.pb.setValue(0) - 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.setFormat(f"UV v19 ({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 - ) + local obj = getNodeByName "{d['name']}" + if obj != undefined do ( + max modify mode + select obj + local theMod = Unwrap_UVW() + modPanel.addModToSelection theMod ui:on + + -- Usando Channel 2 para o Bake (Corrigido o bug que gerava Channel 3 e deixava as UVs dessincronizadas do motor render) + theMod.setMapChannel 2 + + -- Lógica original do V19 reconstruída para "Pack Custom" real + theMod.flattenMap 45.0 #() 0.01 true 0 true true + theMod.setTVSubObjectMode 3 + theMod.selectFaces #{{1..(theMod.numberPolygons())}} + + -- Configurando o Menu "Arrange Elements" (Pack Custom) exatamente como no Print: + -- Usar Recursivo(1), Padding(0.001), Rescale=True(Normalize), Rotate=False, FillHoles=False + theMod.pack 1 0.001 true false false + + -- Não damos collapseStack a pedido do usuário! ) - 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, "UV V19 (C2)") + except Exception as e: d['item'].setText(1, "Erro UV") + print(f"Erro UV V19 em {d['name']}: {e}") ui.pb.setValue(tot) - ui.pb.setFormat("UV Pack Concluído!") + ui.pb.setFormat("UV Pack V19 Concluído!") + +def close_annoying_windows(): + ms = """( + try ( + local desktopHWND = windows.getDesktopHWND() + local children = windows.getChildrenHWND desktopHWND + local targets = #("Corona", "V-Ray", "Buffer", "Render", "Warning", "Error") + + for output in children do ( + local hwnd = output[1] + local title = output[5] + for t in targets do ( + if (matchPattern title pattern:("*" + t + "*") ignoreCase:true) then ( + try ( UIAccessor.CloseDialog hwnd ) catch () + ) + ) + ) + ) catch () + )""" + rt.execute(ms) + +def inspect_uv(ui): + tgs = ui.get_processable_items() + if len(tgs) != 1: + QtWidgets.QMessageBox.warning(ui, "Aviso", "Selecione exatamente UM objeto na lista para auditar a UV.") + return + + obj_name = tgs[0]['name'] + ms = f"""( + local o = getNodeByName "{obj_name}" + if o != undefined and isValidNode o do ( + select o + max modify mode + if o.modifiers[#Unwrap_UVW] != undefined then ( + modPanel.setCurrentObject o.modifiers[#Unwrap_UVW] + o.modifiers[#Unwrap_UVW].edit() + ) else ( + messageBox "O modificador Unwrap_UVW nao foi encontrado." + ) + ) + )""" + rt.execute(ms) -def process_bake_logic(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 - 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() + if not tgs: return 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) - + # Extract UI variables once + p_bk = ui.edt_p_bake.text().replace("\\", "/") + if not p_bk.endswith("/"): p_bk += "/" + if not os.path.exists(p_bk): os.makedirs(p_bk) + + try: res_val = int(ui.spn_res.value()) + except: res_val = 512 + + cv_passes = ui.spn_passes.value() + cv_native = "true" if ui.chk_native.isChecked() else "false" + + tgt_uv_state = 2 if ui.rdo_uv2.isChecked() else 1 # 1=Replace, 2=Multi-UV + for i, d in enumerate(tgs): - if ui._is_cancelled: 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.setValue(i) QtWidgets.QApplication.processEvents() - print(f"\n--- Preparando Bake: {d['name']} ---") + obj_name = d['name'] - o = rt.getNodeByName(d['name']) - if not o: - print("🚨 ERRO: Objeto não encontrado.") - continue + res_str = d['item'].text(3).replace("px", "") + sz = int(res_str) if res_str.isdigit() else res_val - 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 + mprint(f"--- Iniciando script V19 portado para: {obj_name} ---") + + ms_v19_block = f"""( + fn runBake = ( + local objName = "{obj_name}" + local obj = getNodeByName objName - 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] + 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 ) - 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 + ------------------------------------------------------------------------- + -- 2. PREPARE BAKE ELEMENTS + ------------------------------------------------------------------------- + obj.INodeBakeProperties.removeAllBakeElements() + local be = undefined + local fileExt = ".jpg" + + local curRen = renderers.current + local isCorona = matchPattern (curRen as string) pattern:"*Corona*" + local isVRay = matchPattern (curRen as string) pattern:"*V_Ray*" + + if isVRay then ( try ( be = VRayCompleteMap() ) catch ( be = CompleteMap() ) ) + else if isCorona then ( try ( be = Corona_Beauty() ) catch ( try ( be = CShading_Beauty() ) catch ( be = CompleteMap() ) ) ) + else ( be = CompleteMap() ) + + if be == undefined then be = CompleteMap() + + be.outputSzX = {sz} + be.outputSzY = {sz} be.fileType = fileExt - 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 + local pBk = @"{p_bk}" + be.filename = (pBk + obj.name + "_Baked" + fileExt) + try ( be.fileOut = be.filename ) catch() - 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() )""") + local fileExists = doesFileExist be.filename + local skipRender = false -- Overwrite always enabled via UI integration + + if skipRender then ( + print "Skipped (Exists)" + ) else ( + obj.INodeBakeProperties.addBakeElement be + obj.INodeBakeProperties.bakeEnabled = true + obj.INodeBakeProperties.bakeChannel = 2 + + if isCorona and ({cv_native} == false) then ( + try(renderers.current.pass_limit = {cv_passes}; renderers.current.time_limit = 0; renderers.current.noise_level_limit = 0.0)catch() + ) - except Exception as e: - d['item'].setText(1, "Erro Render") - print(f"🚨 EXCEÇÃO PYTHON: {e}") + local wasCancelled = false + try ( + print "Ativando Silenciador de Popups (DialogMonitorOPS)..." + DialogMonitorOPS.unRegisterNotification id:#vr4_kill + DialogMonitorOPS.RegisterNotification vr4life_popup_killer id:#vr4_kill + DialogMonitorOPS.enabled = true + + if doesFileExist be.filename do ( try ( deleteFile be.filename ) catch() ) + + -- DESLIGA NA MARRA RENDER DISTRIBUIDO E TEST RESOLUTION DO V-RAY PARA PARAR DE ABORTAR O BAKE + local cR = renderers.current + if (matchPattern (cR as string) pattern:"*V_Ray*") do ( + try(cR.system_distributedRender = false)catch() + try(cR.image_testResolution = false)catch() + ) + + print "Disparando Render API V19..." + -- Fix 2024: explicit outputfile to guarantee disk writing and prevent black/missing files + render rendertype:#bakeSelected vfb:true progressBar:true quiet:true outputSize:[{sz}, {sz}] outputfile:be.filename cancelled:&wasCancelled + ) catch ( + print "Render Error/Cancel Caught." + wasCancelled = true + ) + + print "Desativando Silenciador de Popups..." + DialogMonitorOPS.enabled = false + DialogMonitorOPS.unRegisterNotification id:#vr4_kill + + if wasCancelled then return -1 + ) + + ------------------------------------------------------------------------- + -- 4. MATERIAL CREATION + ------------------------------------------------------------------------- + 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 fileFound then ( + local bmpTex = BitmapTexture filename:be.filename + if {tgt_uv_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) + return -2 + ) + + ------------------------------------------------------------------------- + -- 5. FINAL CLEANUP + ------------------------------------------------------------------------- + sleep 1.0 + obj.material = newMat + + if {tgt_uv_state} == 1 then ( + channelInfo.CopyChannel obj 3 2 + 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.setFormat("Bake Concluído!") + ui.pb.setFormat("Bake V19 Concluído!") + print("\n" + "="*50) - print("✅ FIM DO LOG DE BAKE") + print("✅ FIM DO LOG DE BAKE V19") print("="*50 + "\n") 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): a_r = ui.chk_a_res.isChecked() diff --git a/vr4life_ui.py b/vr4life_ui.py index ecaa3d8..f303e55 100644 --- a/vr4life_ui.py +++ b/vr4life_ui.py @@ -1,4 +1,5 @@ import os +import json from pymxs import runtime as rt try: from PySide6 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): 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 + self.setWindowTitle("VR4LIFE AUTO-BAKE V312 - MODULAR ENTERPRISE") + 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) self.init_ui() - rt.clearListener(); eng.load_bake_elements(self) + 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;") + # --- 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) 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.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.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_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) + 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") # 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) 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) + + 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") 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) 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") 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.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.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.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())) + 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_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: 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) - - 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) + 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): f = QtWidgets.QFileDialog.getExistingDirectory(self, "Selecione a Pasta GLB") if f: self.edt_p_glb.setText(f + "\\") @@ -132,13 +180,53 @@ class AutoBakeManager(QtWidgets.QDialog): 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) + # 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() \ No newline at end of file + if not self._is_cancelled: eng.prepare_mesh_v19(self) + + 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.pb.setFormat("⚠️ OPERAÇÃO CANCELADA PELO USUÁRIO") + self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: white; background: #FF0000; }") \ No newline at end of file