🚀 Release Notes - VR4Life Plugin v312
🐛 Correções de Bugs (Bugfixes): Fix (Bake Engine): Resolvido o erro crítico "ERROR: Texture file not found" (Textura preta ou ausente). Adicionado e forçado o parâmetro outputfile diretamente na API de render do 3ds Max, garantindo que o V-Ray/Corona grave fisicamente o arquivo JPG/PNG no disco. Fix (Sincronia de UVs): Corrigido o bug onde o "Mapeamento Perfeito" sumia. O botão Prepare UV (P5) estava abrindo a malha no Canal 3, enquanto o render procurava no Canal 2. O processo de Packing foi centralizado definitivamente no Canal 2. Fix (Destruição de UV na Exportação): O motor de exportação GLB foi severamente refatorado. Ele não rouba mais canais de UV, não altera as emissões do Material PBR e nem colapsa o objeto original da sua cena. Ele agora clona a malha em background, exporta de forma limpa e deleta o clone, mantendo sua Viewport intocada. ✨ Melhorias e Novas Funcionalidades (Features): Portabilidade V19 (Core): A lógica raiz, robusta e confiável de colapso de textura do MaxScript (v19 original) foi 100% recriada e injetada com sucesso dentro da estrutura PySide/Python modular atual ( vr4life_engine.py ). Preservação do Modificador Unwrap: Removido o comando de collapseStack automático ao fim do processo de Bake. Agora a ferramenta de Bake aplica a textura perfeitamente, mas preserva a sua pilha de modificadores para edições futuras no modifier Unwrap UVW. Interface do Usuário (UI): Título da ferramenta e cabeçalho principal atualizados para refletir a nova estabilidade (Versão movida da V257 para V312 com data e hora de modificação assinadas na tela principal).main
parent
4886aa2620
commit
5e779cf9ec
@ -0,0 +1,61 @@
|
||||
import os
|
||||
from pymxs import runtime as rt
|
||||
|
||||
def install_vr4life_2026_fixed():
|
||||
try:
|
||||
# 1. CAMINHOS
|
||||
plugin_dir = os.path.dirname(os.path.realpath(__file__)).replace("\\", "/")
|
||||
|
||||
# Onde o MNX foi copiado pelo seu instalador
|
||||
pasta_macros = rt.getDir(rt.name("userMacros"))
|
||||
pasta_enu = os.path.dirname(pasta_macros)
|
||||
caminho_mnx = os.path.join(pasta_enu, "en-US", "UI", "vr4life.mnx").replace("\\", "/")
|
||||
|
||||
# 2. REGISTRAR AS MACROS (Obrigatorio para o MNX funcionar)
|
||||
# Usamos exatamente a categoria "Immerse Games" que está no seu MNX
|
||||
macro_cmd = f"""
|
||||
macroScript VR4Life_Launcher
|
||||
category:"Immerse Games"
|
||||
tooltip:"Abrir VR4Life"
|
||||
(
|
||||
on execute do python.executeFile "{plugin_dir}/run_vr4life.py"
|
||||
)
|
||||
|
||||
macroScript VR4Life_Update
|
||||
category:"Immerse Games"
|
||||
tooltip:"Atualizar VR4Life"
|
||||
(
|
||||
on execute do python.executeFile "{plugin_dir}/vr4life_updater.py"
|
||||
)
|
||||
"""
|
||||
rt.execute(macro_cmd)
|
||||
|
||||
# 3. CARREGAR O MNX VIA INTERFACE DE CONTEXTO (Abordagem Babylon 2026)
|
||||
# Se o menuMan é undefined, usamos o carregamento de arquivo de configuração
|
||||
setup_script = f"""
|
||||
(
|
||||
local mnxPath = "{caminho_mnx}"
|
||||
if (doesFileExist mnxPath) then (
|
||||
-- No 2026, tentamos carregar o arquivo de menus diretamente
|
||||
-- sem passar pelas propriedades de "getMainMenuBar"
|
||||
try (
|
||||
menuMan.loadMenuFile mnxPath
|
||||
menuMan.updateMenuBar()
|
||||
) catch (
|
||||
-- Se falhar aqui, o Max carregará no próximo boot
|
||||
-- pois o arquivo 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()
|
||||
@ -0,0 +1,15 @@
|
||||
import sys, os
|
||||
import importlib
|
||||
|
||||
# Garante que o 3ds Max enxergue a sua pasta de plugins
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
if script_dir not in sys.path:
|
||||
sys.path.append(script_dir)
|
||||
|
||||
# Importa a sua nova UI limpa e recarrega para sempre pegar atualizações
|
||||
import vr4life_ui
|
||||
importlib.reload(vr4life_ui)
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = vr4life_ui.AutoBakeManager()
|
||||
app.show()
|
||||
@ -0,0 +1,510 @@
|
||||
|
||||
macroScript AutoBakeGLB_V45_2
|
||||
category:"Paulo Tools"
|
||||
tooltip:"Bake v45.2"
|
||||
buttonText:"BAKE v45.2"
|
||||
(
|
||||
-- Close existing dialogs
|
||||
try(destroyDialog rolloutBakeGLB)catch()
|
||||
|
||||
-- STRUCTURE TO HOLD DATA
|
||||
struct BakeItemData ( node, listItem )
|
||||
|
||||
rollout rolloutBakeGLB "Auto-Bake Manager V45.2" width:600 height:860
|
||||
(
|
||||
-- HEADER
|
||||
label lblTitle "VR4LIFE" align:#center height:30 css:"font-size:30px; font-weight:bold"
|
||||
label lblVer "v45.2 (Fix Calc)" align:#center height:20 css:"font-size:12px; color:gray" offset:[0,-5]
|
||||
|
||||
-- ====================================================================================
|
||||
-- 1. CONFIGURATION GROUP
|
||||
-- ====================================================================================
|
||||
group "1. Output & Tools"
|
||||
(
|
||||
edittext edtPath "Folder:" text:"" fieldWidth:360 align:#left across:2
|
||||
button btnBrowse "..." width:30 height:18 align:#right offset:[0,-2]
|
||||
|
||||
edittext edtName "GLB Name:" text:"Scene_Exported" fieldWidth:200 align:#left across:2
|
||||
button btnOpenFolder "Open Folder" width:120 height:22 align:#right
|
||||
|
||||
button btnFix "FIX GEOMETRY (Explode & Poly)" width:280 height:30 css:"background-color:#ffcccc; font-weight:bold" across:2 align:#left
|
||||
button btnCheck "CHECK & ADD UNWRAP (Ch. 2)" width:280 height:30 align:#right
|
||||
|
||||
spinner spnPolyPercent "Reduc. %:" range:[1.0, 100.0, 50.0] type:#float fieldWidth:50 across:3 align:#left offset:[0,4]
|
||||
spinner spnPolyLimit "Max Verts:" range:[100, 1000000, 40000] type:#integer fieldWidth:60 align:#left offset:[0,4]
|
||||
button btnReduce "REDUCE (ProOptimizer)" width:160 height:25 align:#right
|
||||
|
||||
button btnInspect "INSPECT UV (Select 1)" width:580 height:30 align:#center
|
||||
)
|
||||
|
||||
group "2. Render Configuration"
|
||||
(
|
||||
radiobuttons rdoEngine labels:#("V-Ray", "Corona", "Arnold") default:1 columns:3 across:2 align:#left
|
||||
spinner spnSize "Size:" range:[128, 8192, 512] type:#integer fieldwidth:50 align:#right
|
||||
|
||||
spinner spnPasses "Pass Limit:" range:[1, 999, 5] type:#integer fieldwidth:50 across:2 align:#left
|
||||
checkbox chkUseNative "Use F10 Settings (Ignore Limits)" checked:false offset:[0, 4] align:#right
|
||||
|
||||
button btnConfig "Open F10 Config" width:280 height:25 across:2 align:#left
|
||||
checkbox chkShowVFB "Show Render Window" checked:false offset:[0, 4] align:#right
|
||||
|
||||
radiobuttons rdoFinalUV labels:#("UV 1 (Replace)", "UV 2 (Multi-UV)") default:1 columns:2 across:2 align:#left
|
||||
checkbox chkAutoFlatten "Re-Flatten + Pack" checked:true align:#right
|
||||
|
||||
checkbox chkOverwrite "Overwrite Existing Files" checked:false align:#center css:"color:red"
|
||||
)
|
||||
|
||||
-- ====================================================================================
|
||||
-- 2. LIST MANAGER
|
||||
-- ====================================================================================
|
||||
dotNetControl lvObjects "System.Windows.Forms.ListView" width:580 height:350 pos:[10, 350]
|
||||
|
||||
-- button btnAddSel "POST SELECTION TO LIST" width:580 height:40 pos:[10, 710] css:"font-weight:bold"
|
||||
-- button btnClear "CLEAR LIST" width:280 height:40 pos:[310, 710]
|
||||
|
||||
progressbar pbProg "Progress:" color:green height:20 width:580 pos:[10, 760]
|
||||
|
||||
button btnBakeChecked "BAKE CHECKED ITEMS" width:580 height:50 pos:[10, 790] css:"font-weight:bold; font-size:14px; background-color:#ccffcc"
|
||||
button btnExportSel "EXPORT SELECTED (1+ Obj)" width:580 height:40 pos:[10, 850]
|
||||
label lblStatus "Ready" pos:[10, 900] width:580 align:#center
|
||||
|
||||
|
||||
-- LOCAL VARIABLES
|
||||
-- ====================================================================================
|
||||
local bakeItems = #()
|
||||
local userCancelled = false
|
||||
local ignoreCheckEvent = false
|
||||
|
||||
-- ====================================================================================
|
||||
-- HELPER FUNCTIONS
|
||||
-- ====================================================================================
|
||||
|
||||
fn InitListView = (
|
||||
lvObjects.View = (dotNetClass "System.Windows.Forms.View").Details
|
||||
lvObjects.FullRowSelect = true
|
||||
lvObjects.GridLines = true
|
||||
lvObjects.CheckBoxes = true
|
||||
lvObjects.MultiSelect = true
|
||||
|
||||
lvObjects.Columns.Add "Object Name" 280
|
||||
lvObjects.Columns.Add "Status" 120
|
||||
lvObjects.Columns.Add "Polys" 80
|
||||
)
|
||||
|
||||
fn UpdateItemStatus item status colorCode = (
|
||||
item.SubItems.Item[1].Text = status
|
||||
item.ForeColor = colorCode
|
||||
-- lvObjects.Refresh() -- Refreshing every item causes flicker, max handles it better usually
|
||||
)
|
||||
|
||||
fn AutoPackUV obj channel = (
|
||||
try (
|
||||
max modify mode
|
||||
select obj
|
||||
modPanel.addModToSelection (Unwrap_UVW ()) ui:on
|
||||
theMod = obj.modifiers[#Unwrap_UVW]
|
||||
if theMod != undefined then (
|
||||
theMod.setMapChannel channel
|
||||
theMod.flattenMap 45.0 #() 0.01 true 0 true true
|
||||
theMod.setTVSubObjectMode 3
|
||||
theMod.selectFaces #{1..(theMod.numberPolygons())}
|
||||
theMod.pack 1 0.001 true false true
|
||||
collapseStack obj
|
||||
)
|
||||
) catch ( print ("Pack UV Error: " + obj.name) )
|
||||
)
|
||||
|
||||
fn ExportToGLB objs path = (
|
||||
if objs.count == 0 then return false
|
||||
select objs
|
||||
-- MANUAL EXPORT DIALOG (File Picker)
|
||||
local filename = getSaveFileName caption:"Export GLB" types:"GL Transmission Format (*.glb)|*.glb|All|*.*" initialDir:path filename:edtName.text
|
||||
|
||||
if filename != undefined then (
|
||||
try ( exportFile filename selectedOnly:true using:gltf_export; return true )
|
||||
catch ( messageBox "Export Cancelled or Error."; return false )
|
||||
)
|
||||
return false
|
||||
)
|
||||
|
||||
fn CloseAnnoyingWindows = (
|
||||
try (
|
||||
local desktopHWND = windows.getDesktopHWND()
|
||||
local children = windows.getChildrenHWND desktopHWND
|
||||
local targets = #("Corona", "V-Ray", "Buffer", "Render", "Warning", "Error")
|
||||
|
||||
for output in children do (
|
||||
local hwnd = output[1]
|
||||
local title = output[5]
|
||||
for t in targets do (
|
||||
if (matchPattern title pattern:("*" + t + "*") ignoreCase:true) then (
|
||||
try ( UIAccessor.CloseDialog hwnd ) catch ()
|
||||
-- try ( windows.sendMessage hwnd 0x0010 0 0 ) catch () -- Alternative Close
|
||||
)
|
||||
)
|
||||
)
|
||||
) catch ()
|
||||
)
|
||||
|
||||
-- ====================================================================================
|
||||
-- CORE LOGIC: BAKE ONE ITEM
|
||||
-- ====================================================================================
|
||||
fn BakeOneObject objData = (
|
||||
obj = objData.node
|
||||
item = objData.listItem
|
||||
|
||||
-- Update UI
|
||||
item.EnsureVisible()
|
||||
UpdateItemStatus item "Baking..." (dotNetClass "System.Drawing.Color").OrangeRed
|
||||
|
||||
-- Validate
|
||||
if (isValidNode obj == false) then ( UpdateItemStatus item "Deleted" (dotNetClass "System.Drawing.Color").Red; return false)
|
||||
|
||||
select obj
|
||||
max modify mode
|
||||
|
||||
try (
|
||||
-------------------------------------------------------------------------
|
||||
-- 1. PREPARE GEOMETRY & UV
|
||||
-------------------------------------------------------------------------
|
||||
if (classof obj != Editable_Poly) then convertToPoly obj
|
||||
|
||||
needNewUV = true
|
||||
if (chkAutoFlatten.checked == false) then (
|
||||
if (polyop.getMapSupport obj 2) then needNewUV = false
|
||||
else ( print "No UVs found on Channel 2. Auto-generating..." )
|
||||
)
|
||||
|
||||
if needNewUV then ( AutoPackUV obj 2; collapseStack obj )
|
||||
|
||||
-------------------------------------------------------------------------
|
||||
-- 2. PREPARE BAKE ELEMENTS
|
||||
-------------------------------------------------------------------------
|
||||
obj.INodeBakeProperties.removeAllBakeElements()
|
||||
be = undefined
|
||||
fileExt = ".jpg" -- Default
|
||||
|
||||
-- Engine Detection
|
||||
curRen = renderers.current
|
||||
isCorona = matchPattern (curRen as string) pattern:"*Corona*"
|
||||
isVRay = matchPattern (curRen as string) pattern:"*V_Ray*"
|
||||
isArnold = matchPattern (curRen as string) pattern:"*Arnold*"
|
||||
|
||||
case rdoEngine.state of (
|
||||
1: ( try ( be = VRayCompleteMap() ) catch ( ) )
|
||||
2: ( try ( be = Corona_Beauty() ) catch ( try ( be = CShading_Beauty() ) catch ( ) ) )
|
||||
3: ( be = CompleteMap() )
|
||||
)
|
||||
if be == undefined then be = CompleteMap()
|
||||
|
||||
be.outputSzX = spnSize.value
|
||||
be.outputSzY = spnSize.value
|
||||
be.fileType = fileExt
|
||||
-- SMART SKIP: Remove timestamp to allow stable file checking
|
||||
be.filename = (edtPath.text + obj.name + "_Baked" + fileExt)
|
||||
|
||||
fileExists = doesFileExist be.filename
|
||||
skipRender = (fileExists) and (chkOverwrite.checked == false)
|
||||
|
||||
if skipRender then (
|
||||
UpdateItemStatus item "Skipped (Exists)" (dotNetClass "System.Drawing.Color").Blue
|
||||
) else (
|
||||
-- PREPARE RENDER
|
||||
obj.INodeBakeProperties.addBakeElement be
|
||||
obj.INodeBakeProperties.bakeEnabled = true
|
||||
obj.INodeBakeProperties.bakeChannel = 2
|
||||
|
||||
-- HARD ENFORCE CORONA LIMITS
|
||||
if isCorona and (chkUseNative.checked == false) then (
|
||||
try(renderers.current.pass_limit = spnPasses.value; renderers.current.time_limit = 0; renderers.current.noise_level_limit = 0.0)catch()
|
||||
)
|
||||
|
||||
wasCancelled = false
|
||||
|
||||
-- RENDER EXECUTION
|
||||
try (
|
||||
render rendertype:#bakeSelected vfb:chkShowVFB.checked progressBar:true quiet:true outputSize:[spnSize.value, spnSize.value] cancelled:&wasCancelled
|
||||
if wasCancelled then userCancelled = true
|
||||
) catch (
|
||||
userCancelled = true
|
||||
print "Render Error/Cancel Caught."
|
||||
)
|
||||
|
||||
if userCancelled then ( UpdateItemStatus item "Cancelled" (dotNetClass "System.Drawing.Color").Red; return false )
|
||||
)
|
||||
|
||||
-------------------------------------------------------------------------
|
||||
-- 4. MATERIAL CREATION
|
||||
-------------------------------------------------------------------------
|
||||
newMat = PhysicalMaterial()
|
||||
newMat.name = (obj.name + "_Baked_Mat")
|
||||
newMat.base_color = color 255 255 255
|
||||
newMat.roughness = 1.0
|
||||
newMat.emission = 0.0 -- Lit
|
||||
newMat.emission_color = color 0 0 0
|
||||
|
||||
-- Wait for File Logic
|
||||
fileFound = false
|
||||
if (skipRender) then (
|
||||
fileFound = true -- File existed, we verified it before
|
||||
) else (
|
||||
-- We rendered, so we wait/verify
|
||||
waitForFile = 0
|
||||
while (waitForFile < 50) and (fileFound == false) do (
|
||||
if doesFileExist be.filename then fileFound = true
|
||||
else ( sleep 0.1; waitForFile += 1 )
|
||||
)
|
||||
)
|
||||
|
||||
if fileFound then (
|
||||
bmpTex = BitmapTexture filename:be.filename
|
||||
if rdoFinalUV.state == 2 then bmpTex.coords.mapChannel = 2 else bmpTex.coords.mapChannel = 1
|
||||
newMat.base_color_map = bmpTex
|
||||
showTextureMap newMat newMat.base_color_map on
|
||||
) else (
|
||||
print ("ERROR: Texture file not found: " + be.filename)
|
||||
UpdateItemStatus item "Tex Missing" (dotNetClass "System.Drawing.Color").Red
|
||||
return false
|
||||
)
|
||||
|
||||
-------------------------------------------------------------------------
|
||||
-- 5. FINAL CLEANUP
|
||||
-------------------------------------------------------------------------
|
||||
CloseAnnoyingWindows() -- Kill windows (V43)
|
||||
sleep 1.0 -- Breathing room
|
||||
|
||||
obj.material = newMat
|
||||
|
||||
if rdoFinalUV.state == 1 then (
|
||||
channelInfo.CopyChannel obj 3 2
|
||||
channelInfo.PasteChannel obj 3 1
|
||||
channelInfo.ClearChannel obj 2
|
||||
collapseStack obj
|
||||
) else ( collapseStack obj )
|
||||
|
||||
UpdateItemStatus item "DONE" (dotNetClass "System.Drawing.Color").Green
|
||||
return true
|
||||
|
||||
) catch (
|
||||
print ("Critical Error on Object: " + obj.name)
|
||||
print (getCurrentException())
|
||||
UpdateItemStatus item "Error" (dotNetClass "System.Drawing.Color").Red
|
||||
return false
|
||||
)
|
||||
)
|
||||
|
||||
-- ====================================================================================
|
||||
-- EVENT HANDLERS
|
||||
-- ====================================================================================
|
||||
on rolloutBakeGLB open do (
|
||||
defaultPath = maxFilePath + "_BAKED\\"
|
||||
if maxFilePath == "" then defaultPath = "C:\\Temp_Bake\\"
|
||||
edtPath.text = defaultPath
|
||||
InitListView()
|
||||
spnPasses.enabled = (not chkUseNative.checked)
|
||||
)
|
||||
|
||||
on lvObjects ItemChecked arg do (
|
||||
if ignoreCheckEvent then return ()
|
||||
if arg.Item.Checked then (
|
||||
ignoreCheckEvent = true
|
||||
-- Uncheck everyone else
|
||||
for i = 0 to lvObjects.Items.Count-1 do (
|
||||
if lvObjects.Items.Item[i] != arg.Item then lvObjects.Items.Item[i].Checked = false
|
||||
)
|
||||
ignoreCheckEvent = false
|
||||
)
|
||||
)
|
||||
|
||||
on lvObjects ItemSelectionChanged arg do (
|
||||
if arg.IsSelected then (
|
||||
-- Find node in struct array
|
||||
local selectedNode = undefined
|
||||
for b in bakeItems do (
|
||||
if b.listItem == arg.Item then (
|
||||
selectedNode = b.node
|
||||
exit
|
||||
)
|
||||
)
|
||||
if isValidNode selectedNode then (
|
||||
select selectedNode
|
||||
-- Optional: max zoomext selected
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
on chkUseNative changed state do ( spnPasses.enabled = (not state) )
|
||||
|
||||
on btnBrowse pressed do (
|
||||
newPath = getSavePath caption:"Save to:" initialDir:edtPath.text
|
||||
if newPath != undefined then edtPath.text = (newPath + "\\")
|
||||
)
|
||||
on btnOpenFolder pressed do ( ShellLaunch edtPath.text "" )
|
||||
on btnConfig pressed do ( renderSceneDialog.open() )
|
||||
|
||||
on btnFix pressed do (
|
||||
undo "Fix Geometry" on (
|
||||
local sel = selection as array
|
||||
if sel.count == 0 then (messageBox "Select objects in viewport first!"; return false)
|
||||
local groupHeads = for o in sel where isGroupHead o collect o
|
||||
for g in groupHeads do ( if isValidNode g then explodeGroup g )
|
||||
|
||||
-- Re-select
|
||||
max modify mode
|
||||
local finalSel = selection as array
|
||||
|
||||
-- AUTO-POPULATE LIST V24 (ADDITIVE)
|
||||
local countAdded = 0
|
||||
|
||||
for obj in finalSel do (
|
||||
-- STRICT FILTER: Geometry Only, No Lights, No Cameras, No Helpers
|
||||
if (isValidNode obj) and (superclassof obj == GeometryClass) and (classof obj != TargetObject) and (isGroupHead obj == false) then (
|
||||
|
||||
-- Always Ensure Poly (Re-Fix)
|
||||
if canConvertTo obj Editable_Poly then convertToPoly obj
|
||||
|
||||
-- Check for Duplicates
|
||||
local isDuplicate = false
|
||||
for b in bakeItems do ( if b.node == obj then isDuplicate = true )
|
||||
|
||||
if (not isDuplicate) then (
|
||||
li = lvObjects.Items.Add obj.name
|
||||
li.SubItems.Add "Pending"
|
||||
-- Use getPolygonCount for safety (returns array: #(faces, verts))
|
||||
local fCount = (getPolygonCount obj)[1]
|
||||
li.SubItems.Add (fCount as string)
|
||||
li.Checked = false
|
||||
append bakeItems (BakeItemData node:obj listItem:li)
|
||||
countAdded += 1
|
||||
)
|
||||
)
|
||||
)
|
||||
if countAdded > 0 then messageBox (countAdded as string + " Object(s) Added to List.")
|
||||
else messageBox "Geometry Re-Fixed (No new items added)."
|
||||
)
|
||||
)
|
||||
|
||||
on btnCheck pressed do (
|
||||
undo "Check UVs" on (
|
||||
objsToCheck = selection as array
|
||||
local countAdded = 0
|
||||
max modify mode
|
||||
for obj in objsToCheck do (
|
||||
if (superclassof obj == GeometryClass and isGroupHead obj == false) then (
|
||||
if (classof obj != Editable_Poly) then convertToPoly obj
|
||||
|
||||
-- Check if modifier already exists (Ignore internal data support)
|
||||
local hasModifier = false
|
||||
for m in obj.modifiers do ( if classof m == Unwrap_UVW then hasModifier = true )
|
||||
|
||||
if (not hasModifier) then (
|
||||
select obj
|
||||
modPanel.addModToSelection (Unwrap_UVW ()) ui:on
|
||||
obj.modifiers[#Unwrap_UVW].setMapChannel 2
|
||||
countAdded += 1
|
||||
)
|
||||
)
|
||||
)
|
||||
if countAdded > 0 then messageBox ("Unwraps Added to Channel 2: " + countAdded as string)
|
||||
else messageBox "Unwrap Modifier already present on selected object(s)."
|
||||
)
|
||||
)
|
||||
|
||||
on btnReduce pressed do (
|
||||
undo "Reduce Polys" on (
|
||||
local sel = selection as array
|
||||
if sel.count == 0 then (messageBox "Select objects first!"; return false)
|
||||
|
||||
max modify mode
|
||||
for obj in sel do (
|
||||
if (superclassof obj == GeometryClass) and (isGroupHead obj == false) then (
|
||||
if (classof obj != Editable_Poly) then convertToPoly obj
|
||||
|
||||
select obj -- Ensure context
|
||||
local mods = ProOptimizer()
|
||||
addModifier obj mods
|
||||
|
||||
mods.KeepUV = true -- Keep Textures! Keep UV Boundaries off by default for smooth
|
||||
mods.OptimizationMode = 1 -- Protect Borders
|
||||
mods.Calculate = true -- Trigger Calculation Explicitly
|
||||
|
||||
-- Use Percentage AFTER Calculation
|
||||
mods.VertexPercent = spnPolyPercent.value
|
||||
|
||||
-- Update List Count if compatible
|
||||
-- Force refresh of geometry for count
|
||||
local newFCount = (getPolygonCount obj)[1]
|
||||
|
||||
for b in bakeItems do (
|
||||
if b.node == obj then (
|
||||
b.listItem.SubItems.Item[2].Text = (newFCount as string)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
messageBox "ProOptimizer Applied (User Settings) \u0026 List Updated."
|
||||
)
|
||||
)
|
||||
|
||||
on btnInspect pressed do (
|
||||
if selection.count != 1 then (messageBox "Select EXACTLY ONE object."; return false)
|
||||
obj = selection[1]
|
||||
max modify mode
|
||||
if (isGroupHead obj) then (messageBox "It is a group! Use Fix Geometry."; return false)
|
||||
theMod = undefined
|
||||
for m in obj.modifiers do ( if classof m == Unwrap_UVW then theMod = m )
|
||||
if theMod != undefined then (
|
||||
modPanel.setCurrentObject theMod
|
||||
theMod.setMapChannel 2
|
||||
theMod.flattenMap 45.0 #() 0.01 true 0 true true
|
||||
theMod.setTVSubObjectMode 3
|
||||
theMod.selectFaces #{1..(theMod.numberPolygons())}
|
||||
theMod.pack 1 0.001 true false true
|
||||
try ( theMod.edit() ) catch ()
|
||||
) else ( messageBox "No Unwrap found. Use CHECK & ADD first." )
|
||||
)
|
||||
|
||||
on btnBakeChecked pressed do (
|
||||
userCancelled = false
|
||||
|
||||
-- 1. Setup Render Limits GLOBALLY
|
||||
curRen = renderers.current
|
||||
isCorona = matchPattern (curRen as string) pattern:"*Corona*"
|
||||
isVRay = matchPattern (curRen as string) pattern:"*V_Ray*"
|
||||
isArnold = matchPattern (curRen as string) pattern:"*Arnold*"
|
||||
|
||||
makeDir edtPath.text
|
||||
|
||||
-- Store old logic similar to V19...
|
||||
-- For brevity, we assume the BakeOneObject handles the per-object enforcement
|
||||
-- But we should disable global time limits once here if not native
|
||||
|
||||
undo off
|
||||
(
|
||||
suspendEditing()
|
||||
|
||||
for b in bakeItems do (
|
||||
if userCancelled then exit
|
||||
if b.listItem.Checked then (
|
||||
-- Check if already done? Optional. For now we force re-bake if checked.
|
||||
success = BakeOneObject b
|
||||
)
|
||||
)
|
||||
|
||||
resumeEditing()
|
||||
completeRedraw()
|
||||
)
|
||||
|
||||
lblStatus.text = "Finished!"
|
||||
)
|
||||
|
||||
on btnExportSel pressed do (
|
||||
if selection.count >= 1 then (
|
||||
local exportObj = selection as array
|
||||
ExportToGLB exportObj edtPath.text
|
||||
messageBox "Export Process Initiated."
|
||||
) else ( messageBox "Please select AT LEAST ONE object in the viewport." )
|
||||
)
|
||||
)
|
||||
createDialog rolloutBakeGLB height:930
|
||||
)
|
||||
@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MaxMenuTransformations>
|
||||
<CreateTopLevelMenu Id="57652913-7b1a-4282-9e07-4e3e1eecd404" Title="vr4life"/>
|
||||
<CreateMenuAction MenuId="57652913-7b1a-4282-9e07-4e3e1eecd404" Id="e20491dc-6fa6-4f31-8c69-af132733dcd9" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
|
||||
<CreateMenuAction MenuId="57652913-7b1a-4282-9e07-4e3e1eecd404" Id="65904065-eee3-45a9-a954-b2eb5f5f3f16" ActionId="647394-VR4Life_Update`Immerse Games"/>
|
||||
<CreateTopLevelMenu Id="ac98fd17-dda8-4066-aa63-1738a0325d06" Title="Vr4life"/>
|
||||
<DeleteItem Id="ac98fd17-dda8-4066-aa63-1738a0325d06"/>
|
||||
<CreateMenu ParentId="b4779ebb-a6f0-4815-9777-57c01c0b584c" MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Title="Vr4Life"/>
|
||||
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="5fd58e27-1a07-4f84-87af-d274abbc5cf0" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
|
||||
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="53e77ac3-9e84-43ad-ae44-9b8a841f5aec" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
|
||||
<MoveItem DestinationId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" ItemId="53e77ac3-9e84-43ad-ae44-9b8a841f5aec" BeforeId="5fd58e27-1a07-4f84-87af-d274abbc5cf0"/>
|
||||
<DeleteItem Id="5fd58e27-1a07-4f84-87af-d274abbc5cf0"/>
|
||||
<DeleteItem Id="53e77ac3-9e84-43ad-ae44-9b8a841f5aec"/>
|
||||
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="14dc1d27-2b7b-4be7-bb91-a64c076e0bbf" ActionId="647394-VR4Life_Launcher`Immerse Games"/>
|
||||
<CreateMenuAction MenuId="a4abc9ea-2b3f-44d3-bd19-8e3a8accefb6" Id="61dc0777-6d19-492c-b56c-436dd3d167b0" ActionId="647394-VR4Life_Update`Immerse Games"/>
|
||||
<SetItemTitle Id="14dc1d27-2b7b-4be7-bb91-a64c076e0bbf" Title="VR4Life Laucher" ResIdTitle="4b96f0b6-f0e6-45aa-aa8e-a2db386f97e5"/>
|
||||
</MaxMenuTransformations>
|
||||
@ -0,0 +1,124 @@
|
||||
import os, time, zipfile
|
||||
from pymxs import runtime as rt
|
||||
try: from PySide6 import QtWidgets
|
||||
except ImportError: from PySide2 import QtWidgets
|
||||
|
||||
def mock_connect_api(ui):
|
||||
if len(ui.e_hash.text()) < 5: QtWidgets.QMessageBox.warning(ui, "Acesso Negado", "Hash inválida."); return
|
||||
ui.b_conn.setText("Conectando..."); QtWidgets.QApplication.processEvents(); time.sleep(1)
|
||||
for w in [ui.c_chn, ui.c_pst, ui.e_tit, ui.e_mp3, ui.b_mp3]: w.setEnabled(True); w.setStyleSheet("background: white; color: black; font-weight: bold;")
|
||||
ui.c_chn.clear(); ui.c_chn.addItems(["Canal: Imóveis Virtuais", "Canal: Showroom Zombisco"])
|
||||
ui.c_pst.clear(); ui.c_pst.addItems(["[ + Cadastrar Nova Postagem ]", "Editar: Apartamento V1"])
|
||||
ui.chk_up.setEnabled(True); ui.chk_up.setStyleSheet("color: #FFD700; font-weight: bold;"); ui.chk_up.setChecked(True)
|
||||
ui.b_conn.setText("✅ Conectado!"); ui.b_conn.setStyleSheet("background: #000; color: #0F0; font-weight: bold; border: 1px solid #0F0;")
|
||||
on_post_changed(ui, 0)
|
||||
|
||||
def on_post_changed(ui, idx):
|
||||
if idx == 0:
|
||||
ui.e_tit.setText(""); ui.e_tit.setPlaceholderText("Título do projeto...")
|
||||
ui.chk_tmb.setChecked(True); ui.chk_tmb.setEnabled(False); ui.chk_tmb.setStyleSheet("color: #00FF00; font-weight: bold;")
|
||||
else:
|
||||
ui.e_tit.setText(ui.c_pst.currentText().replace("Editar: ", ""))
|
||||
ui.chk_tmb.setEnabled(True); ui.chk_tmb.setStyleSheet("color: white; font-weight: bold;")
|
||||
|
||||
def finalize_export(ui, p_bk, p_glb):
|
||||
if not os.path.exists(p_glb): os.makedirs(p_glb)
|
||||
fo = []; rt.execute("max modify mode"); tgs = ui.get_processable_items()
|
||||
|
||||
ui.pb.setFormat("Preparando materiais VR (Multi-ID)..."); QtWidgets.QApplication.processEvents()
|
||||
|
||||
for d in tgs:
|
||||
if ui._is_cancelled: break
|
||||
if "Bake" in d['item'].text(1) or "Já existe" in d['item'].text(1) or "Mat" in d['item'].text(1):
|
||||
try:
|
||||
max_id = rt.execute(f"""(local o = getNodeByName "{d['name']}"; local mid = 1; if o != undefined and isKindOf o Editable_Poly do ( for f = 1 to (polyop.getNumFaces o) do ( local id = polyop.getFaceMatID o f; if id > mid do mid = id ) ); mid)""")
|
||||
|
||||
if max_id > 1:
|
||||
# CONSTRUTOR DE MULTI-MATERIAL (Para objetos que foram processados pelo P4: Multi-UV)
|
||||
ms_mat = f"""(
|
||||
local o = getNodeByName "{d['name']}"
|
||||
local mm = MultiMaterial numsubs:{int(max_id)} name:(o.name + "_MultiMat")
|
||||
for i = 1 to {int(max_id)} do (
|
||||
local ip = @"{p_bk}/{d['name']}_B_ID" + (i as string) + ".jpg"
|
||||
if doesFileExist ip do (
|
||||
local m = PhysicalMaterial name:(o.name + "_MatID_" + (i as string))
|
||||
m.base_color_map = BitmapTexture filename:ip
|
||||
m.roughness = 1.0; m.metalness = 0.0; try(m.reflectivity = 0.0; m.reflection_weight = 0.0)catch()
|
||||
mm[i] = m
|
||||
)
|
||||
)
|
||||
o.material = mm
|
||||
)"""
|
||||
rt.execute(ms_mat)
|
||||
d['item'].setText(1, "Mat OK (Multi)")
|
||||
fo.append(rt.getNodeByName(d['name']))
|
||||
|
||||
else:
|
||||
# MATERIAL PADRÃO (Para objetos normais que cabem numa textura só)
|
||||
ip = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/")
|
||||
if os.path.exists(ip):
|
||||
rt.execute(f"""(
|
||||
local o = getNodeByName "{d['name']}"
|
||||
if o != undefined do (
|
||||
local m = PhysicalMaterial name:(o.name + "_Mat"); m.base_color_map = BitmapTexture filename:@"{ip}"
|
||||
m.roughness = 1.0; m.metalness = 0.0; try(m.reflectivity = 0.0; m.reflection_weight = 0.0)catch()
|
||||
o.material = m
|
||||
)
|
||||
)""")
|
||||
fo.append(rt.getNodeByName(d['name'])); d['item'].setText(1, "Mat OK")
|
||||
except Exception as e:
|
||||
d['item'].setText(1, "Erro Mat"); print(f"Erro Material em {d['name']}: {e}")
|
||||
|
||||
if not fo and not ui._is_cancelled:
|
||||
QtWidgets.QMessageBox.warning(ui, "Exportação Interrompida", "Nenhum objeto validado para exportação.")
|
||||
ui.pb.setFormat("Pronto"); ui.pb.setValue(0)
|
||||
return
|
||||
|
||||
if fo and not ui._is_cancelled:
|
||||
rt.select(fo); sn = rt.maxFileName.split(".")[0] if rt.maxFileName else "Cena_VR4LIFE"
|
||||
og = os.path.join(p_glb, f"{sn}.glb").replace("\\", "/"); oz = os.path.join(p_glb, f"{sn}.zip").replace("\\", "/")
|
||||
tp = os.path.join(p_glb, f"{sn}_Thumb.jpg").replace("\\", "/"); mp = ui.e_mp3.text(); hm = os.path.exists(mp)
|
||||
|
||||
to = False
|
||||
if ui.chk_tmb.isChecked() and ui.chk_tmb.isEnabled():
|
||||
ui.pb.setFormat("📸 Thumbnail (1280x720)..."); QtWidgets.QApplication.processEvents()
|
||||
rt.execute(f"""( local c = getActiveCamera(); if c == undefined do ( local ac = for cam in cameras where (classof cam != TargetObject) collect cam; if ac.count > 0 do c = ac[1] ); try ( if c != undefined then ( render camera:c outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) else ( render outputwidth:1280 outputheight:720 outputfile:@"{tp}" vfb:false quiet:true ) ) catch() )""")
|
||||
if os.path.exists(tp): to = True
|
||||
|
||||
ui.pb.setFormat("Limpando Hierarquia e XForm..."); QtWidgets.QApplication.processEvents()
|
||||
|
||||
rt.execute("""
|
||||
(
|
||||
local sel = selection as array
|
||||
for o in sel do (
|
||||
o.parent = undefined
|
||||
ResetXForm o
|
||||
collapseStack o
|
||||
if not isKindOf o Editable_Poly do try(convertToPoly o)catch()
|
||||
)
|
||||
)
|
||||
""")
|
||||
|
||||
ui.pb.setFormat("Montando GLB..."); QtWidgets.QApplication.processEvents()
|
||||
try: rt.exportFile(og, rt.name("noPrompt"), selectedOnly=True, using=rt.GLTFExporter)
|
||||
except: rt.execute(f'exportFile "{og}" #noPrompt selectedOnly:true')
|
||||
|
||||
zo = False
|
||||
if os.path.exists(og):
|
||||
ui.pb.setFormat("🗜️ ZIPando GLB..."); QtWidgets.QApplication.processEvents()
|
||||
try:
|
||||
with zipfile.ZipFile(oz, 'w', zipfile.ZIP_DEFLATED) as zf: zf.write(og, os.path.basename(og))
|
||||
zo = True
|
||||
except: pass
|
||||
|
||||
if ui.chk_up.isChecked() and ui.chk_up.isEnabled() and zo:
|
||||
cs = ui.c_chn.currentText(); ac = "NOVO" if ui.c_pst.currentIndex() == 0 else "ATUALIZAR"; tit = ui.e_tit.text()
|
||||
ui.pb.setFormat(f"📡 API: Enviando..."); QtWidgets.QApplication.processEvents(); time.sleep(1.5)
|
||||
msg = f"Sincronizado!\n\n📡 Canal: {cs}\n📝 Ação: {ac}\n🏷️ Titulo: {tit}\n\n📦 ZIP: {os.path.basename(oz)}\n📸 Thumb: {'Sim' if to else 'Não'}\n🎵 Áudio: {'Sim' if hm else 'Não'}"
|
||||
QtWidgets.QMessageBox.information(ui, "Cloud API", msg)
|
||||
else:
|
||||
msg = f"Exportado:\n{og}";
|
||||
if zo: msg += f"\n\n🗜️ ZIP:\n{oz}"
|
||||
if to: msg += f"\n\n📸 Thumb:\n{tp}"
|
||||
QtWidgets.QMessageBox.information(ui, "Sucesso", msg)
|
||||
ui.pb.setFormat("Pronto"); ui.pb.setValue(0)
|
||||
@ -0,0 +1,521 @@
|
||||
import os, time, subprocess
|
||||
from pymxs import runtime as rt
|
||||
try: from PySide6 import QtWidgets, QtCore, QtGui
|
||||
except ImportError: from PySide2 import QtWidgets, QtCore, QtGui
|
||||
import vr4life_cloud as cld
|
||||
|
||||
def get_vdenoise_path():
|
||||
for k, v in os.environ.items():
|
||||
if "VRAY" in k.upper() and "MAX" in k.upper() and "MAIN" in k.upper():
|
||||
p = os.path.join(v, "vdenoise.exe")
|
||||
if os.path.exists(p): return f'"{p}"'
|
||||
return "vdenoise.exe"
|
||||
|
||||
def load_bake_elements(ui):
|
||||
ui.cmb_bake_elem.clear()
|
||||
found = ["CompleteMap", "VRayCompleteMap", "Corona_Beauty", "CShading_Beauty"]
|
||||
try:
|
||||
r_list = rt.execute("for c in bake_elements.classes collect (c as string)")
|
||||
for c in r_list:
|
||||
if str(c) not in found: found.append(str(c))
|
||||
except: pass
|
||||
ui.cmb_bake_elem.addItems(sorted(list(set(found))))
|
||||
try:
|
||||
rnd = str(rt.execute("renderers.current as string"))
|
||||
t = "VRayCompleteMap" if "V_Ray" in rnd else "Corona_Beauty" if "Corona" in rnd else "CompleteMap"
|
||||
idx = ui.cmb_bake_elem.findText(t)
|
||||
if idx >= 0: ui.cmb_bake_elem.setCurrentIndex(idx)
|
||||
except: pass
|
||||
|
||||
def load_selection(ui):
|
||||
ui.tree.clear(); ui.bake_items = []
|
||||
ms = """( local g = #(); fn ex objs = ( for o in objs do ( if isGroupHead o then ( setGroupOpen o true; ex o.children ) else ( if superclassof o == GeometryClass and (classOf o != TargetObject) and (canConvertTo o Editable_Poly) do appendIfUnique g o ) ) ); ex selection; g )"""
|
||||
sel = rt.execute(ms)
|
||||
if not sel: return
|
||||
tl = [{'name': str(o.name), 'poly': rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0} for o in sel]
|
||||
tl.sort(key=lambda x: x['poly'], reverse=True)
|
||||
for d in tl:
|
||||
i = QtWidgets.QTreeWidgetItem([d['name'], "Pronto", f"{d['poly']:,}", "", ""])
|
||||
i.setFlags(i.flags() | QtCore.Qt.ItemIsUserCheckable)
|
||||
i.setCheckState(0, QtCore.Qt.Checked)
|
||||
ui.tree.addTopLevelItem(i)
|
||||
ui.bake_items.append({'name': d['name'], 'item': i})
|
||||
ui.pb.setFormat(f"Carregados: {len(tl)}")
|
||||
ui.pb.setValue(0)
|
||||
ui.upd_res_col()
|
||||
|
||||
def attach_grouped_objects(ui, from_auto=False):
|
||||
ui.pb.setFormat("Fundindo grupos inteiros...")
|
||||
QtWidgets.QApplication.processEvents()
|
||||
ms = """(
|
||||
fn hasProtectedName nd = (
|
||||
local nm = toLower nd.name
|
||||
if (matchPattern nm pattern:"*_mult*") or (matchPattern nm pattern:"*_catg*") or (matchPattern nm pattern:"*_mat*") do return true
|
||||
for c in nd.children do ( if hasProtectedName c do return true )
|
||||
return false
|
||||
)
|
||||
local act = 0; local new_objs = #(); local orig_sel = selection as array;
|
||||
local loose = for o in orig_sel where not isGroupHead o and not isGroupMember o collect o;
|
||||
local heads = for o in orig_sel where isGroupHead o and not (hasProtectedName o) collect o
|
||||
|
||||
for h in heads do (
|
||||
local valid_cg = for c in h.children where superclassof c == GeometryClass and classof c != TargetObject collect c
|
||||
if valid_cg.count > 0 do (
|
||||
local b = valid_cg[1]
|
||||
if not isKindOf b Editable_Poly do try(convertToPoly b)catch()
|
||||
if isKindOf b Editable_Poly do (
|
||||
for i = 2 to valid_cg.count do (
|
||||
local n = valid_cg[i]
|
||||
if not isKindOf n Editable_Poly do try(convertToPoly n)catch()
|
||||
if isKindOf n Editable_Poly do try(polyOp.attach b n)catch()
|
||||
)
|
||||
b.name = h.name
|
||||
setGroupMember b false
|
||||
append new_objs b
|
||||
act += 1
|
||||
)
|
||||
)
|
||||
try(delete h)catch()
|
||||
)
|
||||
local final_sel = loose; for no in new_objs do append final_sel no; if final_sel.count > 0 do select final_sel; act
|
||||
)"""
|
||||
try:
|
||||
act = rt.execute(ms)
|
||||
load_selection(ui)
|
||||
if not from_auto:
|
||||
if act > 0: QtWidgets.QMessageBox.information(ui, "Sucesso", f"{act} Grupos fundidos.")
|
||||
else: QtWidgets.QMessageBox.information(ui, "Aviso", "Nenhum Grupo encontrado.")
|
||||
except Exception as e: print("Erro Solda:", e)
|
||||
ui.pb.setValue(0); ui.pb.setFormat("Pronto")
|
||||
|
||||
def super_attach_objects(ui, from_auto=False):
|
||||
ui.pb.setFormat("Super Solda VR...")
|
||||
QtWidgets.QApplication.processEvents()
|
||||
ms = """(
|
||||
fn hasProtectedName nd = (
|
||||
local nm = toLower nd.name
|
||||
if (matchPattern nm pattern:"*_mult*") or (matchPattern nm pattern:"*_catg*") or (matchPattern nm pattern:"*_mat*") do return true
|
||||
for c in nd.children do ( if hasProtectedName c do return true )
|
||||
return false
|
||||
)
|
||||
local sel = for o in selection where superclassof o == GeometryClass and classof o != TargetObject and not (hasProtectedName o) collect o
|
||||
if sel.count > 1 do (
|
||||
local b = sel[1]
|
||||
if not isKindOf b Editable_Poly do try(convertToPoly b)catch()
|
||||
if isKindOf b Editable_Poly do (
|
||||
for i = 2 to sel.count do (
|
||||
local n = sel[i]
|
||||
if not isKindOf n Editable_Poly do try(convertToPoly n)catch()
|
||||
if isKindOf n Editable_Poly do try(polyOp.attach b n)catch()
|
||||
)
|
||||
b.name = uniqueName "VR_Bloco_Fundido"
|
||||
select b
|
||||
)
|
||||
)
|
||||
sel.count
|
||||
)"""
|
||||
try:
|
||||
act = rt.execute(ms)
|
||||
load_selection(ui)
|
||||
if not from_auto:
|
||||
if act > 1: QtWidgets.QMessageBox.information(ui, "Sucesso", f"{act} objetos fundidos.")
|
||||
else: QtWidgets.QMessageBox.information(ui, "Aviso", "Selecione pelo menos 2 objetos.")
|
||||
except Exception as e: print("Erro Super Solda:", e)
|
||||
ui.pb.setValue(0); ui.pb.setFormat("Pronto")
|
||||
|
||||
def optimize_geometry(ui):
|
||||
sp = ui.spn_pct.value()
|
||||
tg = ui.spn_min_poly.value()
|
||||
tgs = ui.get_processable_items()
|
||||
tot = len(tgs)
|
||||
ui.pb.setMaximum(tot)
|
||||
ui.pb.setValue(0)
|
||||
rt.execute("max modify mode")
|
||||
|
||||
for i, d in enumerate(tgs):
|
||||
if ui._is_cancelled: break
|
||||
ui.pb.setFormat(f"Opt ({i+1}/{tot}): {d['name']}...")
|
||||
ui.pb.setValue(i)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
o = rt.getNodeByName(d['name'])
|
||||
if o:
|
||||
try:
|
||||
cp = rt.getPolygonCount(o)[0] if rt.canConvertTo(o, rt.Editable_Poly) else 0
|
||||
if cp < 50:
|
||||
d['item'].setText(1, "Geo Base")
|
||||
else:
|
||||
rt.select(o)
|
||||
if not rt.isKindOf(o, rt.Editable_Poly):
|
||||
rt.convertTo(o, rt.Editable_Poly)
|
||||
|
||||
opt = rt.ProOptimizer()
|
||||
rt.addModifier(o, opt)
|
||||
opt.KeepTextures = True
|
||||
opt.KeepNormals = True
|
||||
opt.KeepBorders = True
|
||||
opt.LockMat = True
|
||||
opt.Calculate = True
|
||||
|
||||
current_pct = 100.0
|
||||
p = 0
|
||||
while True:
|
||||
if ui._is_cancelled: break
|
||||
current_pct = current_pct * (sp / 100.0)
|
||||
opt.VertexPercent = current_pct
|
||||
rt.redrawViews()
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
np = rt.getPolygonCount(o)[0]
|
||||
p += 1
|
||||
d['item'].setText(2, f"{np:,}")
|
||||
d['item'].setText(1, f"Opt ({p}x)")
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
if np <= tg or np < 50 or p >= 20 or (np >= cp and p > 1): break
|
||||
cp = np
|
||||
|
||||
rt.collapseStack(o)
|
||||
if not rt.isKindOf(o, rt.Editable_Poly):
|
||||
rt.convertTo(o, rt.Editable_Poly)
|
||||
|
||||
if not ui._is_cancelled:
|
||||
d['item'].setText(2, f"{rt.getPolygonCount(o)[0]:,}")
|
||||
d['item'].setText(1, f"Opt OK ({p}x)")
|
||||
except Exception as e:
|
||||
d['item'].setText(1, "Erro Opt")
|
||||
print(f"Erro Opt {d['name']}: {e}")
|
||||
|
||||
ui.pb.setValue(tot)
|
||||
ui.pb.setFormat("Opt Concluída!")
|
||||
|
||||
def slice_large_objects(ui, from_auto=False):
|
||||
if not ui.bake_items: return
|
||||
thr = ui.spn_max_sz.value()
|
||||
tgs = ui.get_processable_items()
|
||||
tot = len(tgs)
|
||||
ui.pb.setMaximum(tot)
|
||||
ui.pb.setValue(0)
|
||||
rt.execute("max modify mode")
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("🔍 INICIANDO LOG: MULTI-UV (P4)")
|
||||
print(f"-> Tamanho de corte (Threshold): {thr}")
|
||||
print("="*50)
|
||||
|
||||
for i, d in enumerate(tgs):
|
||||
if ui._is_cancelled: break
|
||||
ui.pb.setFormat(f"Multi-UV ({i+1}/{tot}): {d['name']}...")
|
||||
ui.pb.setValue(i)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
print(f"\n--- Processando objeto: {d['name']} ---")
|
||||
|
||||
# CÓDIGO CORRIGIDO: Tudo embrulhado num "fn" (função) nativo para o return funcionar.
|
||||
ms = f"""(
|
||||
fn applyMultiMatID objName thrVal = (
|
||||
local o = getNodeByName objName
|
||||
if o == undefined do return -1
|
||||
if not isKindOf o Editable_Poly do convertToPoly o
|
||||
|
||||
local dx = abs(o.max.x - o.min.x)
|
||||
local dy = abs(o.max.y - o.min.y)
|
||||
local dz = abs(o.max.z - o.min.z)
|
||||
local md = amax #(dx, dy, dz)
|
||||
|
||||
if md <= thrVal do (
|
||||
for f = 1 to (polyop.getNumFaces o) do polyop.setFaceMatID o f 1
|
||||
return 1
|
||||
)
|
||||
|
||||
local numChunks = (ceil (md / thrVal)) as integer
|
||||
if numChunks > 10 do numChunks = 10
|
||||
|
||||
local axis = if md == dx then 1 else if md == dy then 2 else 3
|
||||
local minVal = if axis == 1 then o.min.x else if axis == 2 then o.min.y else o.min.z
|
||||
local step = md / numChunks
|
||||
|
||||
for f = 1 to (polyop.getNumFaces o) do (
|
||||
local center = polyop.getFaceCenter o f
|
||||
local val = if axis == 1 then center.x else if axis == 2 then center.y else center.z
|
||||
local id = (floor ((val - minVal) / step)) as integer + 1
|
||||
if id > numChunks do id = numChunks
|
||||
if id < 1 do id = 1
|
||||
polyop.setFaceMatID o f id
|
||||
)
|
||||
return numChunks
|
||||
)
|
||||
try ( applyMultiMatID "{d['name']}" {thr} ) catch ( -2 )
|
||||
)"""
|
||||
|
||||
try:
|
||||
print("-> Executando MaxScript de Fatiamento Lógico...")
|
||||
r = rt.execute(ms)
|
||||
print(f"-> Resposta do MaxScript: {r}")
|
||||
|
||||
if r == -1:
|
||||
print("🚨 ERRO: Objeto não encontrado na cena pelo MaxScript.")
|
||||
d['item'].setText(1, "Erro: Objeto")
|
||||
elif r == -2:
|
||||
print("🚨 ERRO: MaxScript falhou internamente (Topologia corrompida?).")
|
||||
d['item'].setText(1, "Erro: Script")
|
||||
elif r and int(r) > 1:
|
||||
print(f"✅ SUCESSO: Objeto dividido em {int(r)} IDs!")
|
||||
d['item'].setText(1, f"IDs: {int(r)}")
|
||||
else:
|
||||
print("-> Objeto menor que a Meta. Mantido como 1 ID.")
|
||||
d['item'].setText(1, "1 ID (Normal)")
|
||||
|
||||
except Exception as e:
|
||||
d['item'].setText(1, "Erro Python")
|
||||
print(f"🚨 ERRO CRÍTICO PYTHON no Multi-UV: {e}")
|
||||
|
||||
ui.pb.setValue(tot)
|
||||
ui.pb.setFormat("Multi-IDs Gerados!")
|
||||
print("\n" + "="*50)
|
||||
print("✅ FIM DO LOG MULTI-UV")
|
||||
print("="*50 + "\n")
|
||||
|
||||
def prepare_mesh(ui):
|
||||
tgs = ui.get_processable_items()
|
||||
tot = len(tgs)
|
||||
ui.pb.setMaximum(tot)
|
||||
ui.pb.setValue(0)
|
||||
rt.execute("max modify mode")
|
||||
|
||||
for i, d in enumerate(tgs):
|
||||
if ui._is_cancelled: break
|
||||
ui.pb.setFormat(f"UV ({i+1}/{tot}): {d['name']}...")
|
||||
ui.pb.setValue(i)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
o = rt.getNodeByName(d['name'])
|
||||
if o:
|
||||
rt.select(o)
|
||||
try:
|
||||
ms = f"""(
|
||||
local o = getNodeByName "{d['name']}"
|
||||
if not isKindOf o Editable_Poly do convertToPoly o
|
||||
local m = Unwrap_UVW()
|
||||
addModifier o m
|
||||
m.setMapChannel 3
|
||||
m.setTVSubObjectMode 3
|
||||
local maxID = 1
|
||||
for f = 1 to (polyop.getNumFaces o) do (
|
||||
local id = polyop.getFaceMatID o f
|
||||
if id > maxID do maxID = id
|
||||
)
|
||||
for x = 1 to maxID do (
|
||||
m.selectByMatID x
|
||||
local sel = m.getSelectedFaces()
|
||||
if not sel.isEmpty do (
|
||||
m.flattenMap 45.0 #() 0.002 true 0 true false
|
||||
m.pack 1 0.002 true false false
|
||||
)
|
||||
)
|
||||
collapseStack o
|
||||
)"""
|
||||
rt.execute(ms)
|
||||
if not rt.isKindOf(o, rt.Editable_Poly):
|
||||
rt.convertTo(o, rt.Editable_Poly)
|
||||
d['item'].setText(1, "UV Packed (C3)")
|
||||
except:
|
||||
d['item'].setText(1, "Erro UV")
|
||||
|
||||
ui.pb.setValue(tot)
|
||||
ui.pb.setFormat("UV Pack Concluído!")
|
||||
|
||||
def process_bake_logic(ui, auto_export=False):
|
||||
ui._is_cancelled = False
|
||||
el = ui.cmb_bake_elem.currentText()
|
||||
p_bk = ui.edt_p_bake.text()
|
||||
rnd = str(rt.execute("renderers.current as string"))
|
||||
i_vr = "V_Ray" in rnd
|
||||
i_cor = "Corona" in rnd
|
||||
u_den = ui.chk_denoise.isChecked()
|
||||
a_res = ui.chk_a_res.isChecked()
|
||||
|
||||
if not os.path.exists(p_bk): os.makedirs(p_bk)
|
||||
tgs = ui.get_processable_items()
|
||||
tot = len(tgs)
|
||||
ui.pb.setMaximum(tot)
|
||||
ui.pb.setValue(0)
|
||||
|
||||
print("\n" + "="*50)
|
||||
print("🎬 INICIANDO LOG: MOTOR DE BAKE (P6)")
|
||||
print(f"-> Pasta Alvo: {p_bk}")
|
||||
print(f"-> Renderizador: {rnd}")
|
||||
print("="*50)
|
||||
|
||||
for i, d in enumerate(tgs):
|
||||
if ui._is_cancelled: break
|
||||
ui.pb.setFormat(f"Bake ({i+1}/{tot}): {d['name']}...")
|
||||
ui.pb.setValue(i)
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
print(f"\n--- Preparando Bake: {d['name']} ---")
|
||||
|
||||
o = rt.getNodeByName(d['name'])
|
||||
if not o:
|
||||
print("🚨 ERRO: Objeto não encontrado.")
|
||||
continue
|
||||
|
||||
try:
|
||||
# 1. Conta quantos IDs existem no objeto
|
||||
ms_mid = f"""(local o = getNodeByName "{d['name']}"; local mid = 1; if o != undefined and isKindOf o Editable_Poly do ( for f = 1 to (polyop.getNumFaces o) do ( local id = polyop.getFaceMatID o f; if id > mid do mid = id ) ); mid)"""
|
||||
max_id = rt.execute(ms_mid)
|
||||
if not max_id: max_id = 1
|
||||
max_id = int(max_id)
|
||||
print(f"-> Total de IDs a renderizar: {max_id}")
|
||||
|
||||
rt.execute("max select none")
|
||||
rt.select(o)
|
||||
rt.execute("max modify mode")
|
||||
rt.execute("max zoomext sel all")
|
||||
rt.redrawViews()
|
||||
time.sleep(0.5)
|
||||
|
||||
for mid in range(1, max_id + 1):
|
||||
if ui._is_cancelled: break
|
||||
print(f"\n-> Assando ID: {mid} de {max_id}")
|
||||
|
||||
if max_id == 1:
|
||||
t_jpg_id = os.path.join(p_bk, f"{d['name']}_B.jpg").replace("\\", "/")
|
||||
else:
|
||||
t_jpg_id = os.path.join(p_bk, f"{d['name']}_B_ID{mid}.jpg").replace("\\", "/")
|
||||
|
||||
if os.path.exists(t_jpg_id):
|
||||
d['item'].setText(1, f"Já existe ID{mid}")
|
||||
print("-> Ficheiro já existe. Pulando.")
|
||||
continue
|
||||
|
||||
cr = ui.spn_res.value()
|
||||
if a_res:
|
||||
md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z))
|
||||
cr = 256 if md <= ui.s256.value() else 512 if md <= ui.s512.value() else 1024 if md <= ui.s1024.value() else cr
|
||||
|
||||
ext = ".exr" if (i_vr and u_den) else ".jpg"
|
||||
t_rnd_id = t_jpg_id.replace(".jpg", ".exr") if ext == ".exr" else t_jpg_id
|
||||
|
||||
if max_id > 1:
|
||||
print("-> Isolando UV do ID atual...")
|
||||
# CÓDIGO CORRIGIDO 1: Embrulhado em 'fn'
|
||||
ms_uv = f"""(
|
||||
fn isolateUV currentMid = (
|
||||
global temp_uv_mod = Unwrap_UVW()
|
||||
addModifier $ temp_uv_mod
|
||||
temp_uv_mod.setMapChannel 3
|
||||
temp_uv_mod.setTVSubObjectMode 3
|
||||
local allF = #{{1..(polyop.getNumFaces $)}}
|
||||
temp_uv_mod.selectByMatID currentMid
|
||||
local tgtF = temp_uv_mod.getSelectedFaces()
|
||||
local hideF = allF - tgtF
|
||||
|
||||
if hideF.numberset > 0 do (
|
||||
temp_uv_mod.selectFaces hideF
|
||||
temp_uv_mod.moveSelected [-10, -10, 0]
|
||||
)
|
||||
return 1
|
||||
)
|
||||
try ( isolateUV {mid} ) catch ( 0 )
|
||||
)"""
|
||||
res_uv = rt.execute(ms_uv)
|
||||
if res_uv == 0: print("🚨 ERRO: O MaxScript não conseguiu manipular as UVs.")
|
||||
|
||||
print(f"-> Disparando o Render: {t_rnd_id}")
|
||||
# CÓDIGO CORRIGIDO 2: Embrulhado em 'fn'
|
||||
ms_bake = f"""(
|
||||
fn doBake fileOut szX szY fileExt cor den elem = (
|
||||
try(freeSceneBitmaps(); vfbControl #clearimage)catch()
|
||||
if cor and den do try ( renderers.current.denoise_enable = true ) catch()
|
||||
$.INodeBakeProperties.removeAllBakeElements()
|
||||
|
||||
local be = (execute (elem + "()"))
|
||||
be.outputSzX = szX
|
||||
be.outputSzY = szY
|
||||
be.fileType = fileExt
|
||||
be.fileName = fileOut
|
||||
|
||||
$.INodeBakeProperties.addBakeElement be
|
||||
$.INodeBakeProperties.bakeEnabled = true
|
||||
$.INodeBakeProperties.bakeChannel = 3
|
||||
render rendertype:#bakeSelected vfb:true quiet:true production:true outputfile:fileOut
|
||||
return 1
|
||||
)
|
||||
try ( doBake "{t_rnd_id}" {cr} {cr} "{ext}" {str(i_cor).lower()} {str(u_den).lower()} "{el}" ) catch ( 0 )
|
||||
)"""
|
||||
res_bk = rt.execute(ms_bake)
|
||||
if res_bk == 0: print("🚨 ERRO: O Renderizador rejeitou o comando de Bake!")
|
||||
|
||||
wt = 0
|
||||
while not os.path.exists(t_rnd_id) and wt < 60:
|
||||
time.sleep(0.5)
|
||||
wt += 0.5
|
||||
QtWidgets.QApplication.processEvents()
|
||||
|
||||
if os.path.exists(t_rnd_id):
|
||||
if i_vr and u_den:
|
||||
print("-> Passando Denoise IA...")
|
||||
c_in = t_rnd_id.replace("/", "\\")
|
||||
c_out = t_rnd_id.replace(".exr", "_denoised.exr").replace("/", "\\")
|
||||
try:
|
||||
p = subprocess.Popen(f'{get_vdenoise_path()} -inputFile="{c_in}" -outputFile="{c_out}" -display=0', shell=True, creationflags=0x08000000); dt = 0
|
||||
while p.poll() is None and dt < 60: QtWidgets.QApplication.processEvents(); time.sleep(0.5); dt += 0.5
|
||||
if p.poll() is None: p.terminate()
|
||||
except: pass
|
||||
f_exr = c_out.replace("\\", "/") if os.path.exists(c_out.replace("\\", "/")) else t_rnd_id
|
||||
rt.execute(f"""( try(vfbControl #clearimage)catch(); try ( local bI = openBitmap @"{f_exr}"; if bI != undefined do ( local bO = bitmap bI.width bI.height filename:@"{t_jpg_id}" hdr:true; copy bI bO; save bO; close bI; close bO; free bI; free bO ) ) catch() )""")
|
||||
rt.execute("freeSceneBitmaps(); gc light:true"); time.sleep(0.5)
|
||||
try: os.remove(t_rnd_id); os.remove(c_out.replace("\\", "/"))
|
||||
except: pass
|
||||
|
||||
if max_id > 1:
|
||||
print("-> Removendo modificador temporário de UV...")
|
||||
rt.execute("try(deleteModifier $ globalvars.get #temp_uv_mod)catch()")
|
||||
|
||||
if os.path.exists(t_jpg_id):
|
||||
d['item'].setText(1, "Bake OK")
|
||||
print("-> SUCESSO: Textura gravada no disco.")
|
||||
else:
|
||||
d['item'].setText(1, "Erro Conv")
|
||||
print("🚨 ERRO: A textura não foi salva. Timeout?")
|
||||
|
||||
if not ui._is_cancelled:
|
||||
print("-> Restaurando UV final do objeto...")
|
||||
rt.execute(f"""( local o = getNodeByName "{d['name']}"; try( ChannelInfo.CopyChannel o 3 3; ChannelInfo.PasteChannel o 3 1; collapseStack o )catch() )""")
|
||||
|
||||
except Exception as e:
|
||||
d['item'].setText(1, "Erro Render")
|
||||
print(f"🚨 EXCEÇÃO PYTHON: {e}")
|
||||
|
||||
ui.pb.setValue(tot)
|
||||
ui.pb.setFormat("Bake Concluído!")
|
||||
print("\n" + "="*50)
|
||||
print("✅ FIM DO LOG DE BAKE")
|
||||
print("="*50 + "\n")
|
||||
|
||||
if auto_export and not ui._is_cancelled:
|
||||
cld.finalize_export(ui, p_bk, ui.edt_p_glb.text())
|
||||
|
||||
def upd_res_col(ui):
|
||||
a_r = ui.chk_a_res.isChecked()
|
||||
m_r = ui.spn_res.value()
|
||||
for d in ui.bake_items:
|
||||
o = rt.getNodeByName(d['name'])
|
||||
if o:
|
||||
try:
|
||||
md = max(abs(o.max.x - o.min.x), abs(o.max.y - o.min.y), abs(o.max.z - o.min.z))
|
||||
r = 256 if md <= ui.s256.value() else 512 if md <= ui.s512.value() else 1024 if md <= ui.s1024.value() else m_r if a_r else m_r
|
||||
d['item'].setText(3, f"{r}px")
|
||||
d['item'].setText(4, f"{md:.1f}")
|
||||
if r == 256: d['item'].setForeground(3, QtGui.QColor(0, 255, 255))
|
||||
elif r == 512: d['item'].setForeground(3, QtGui.QColor(0, 255, 0))
|
||||
elif r == 1024: d['item'].setForeground(3, QtGui.QColor(255, 165, 0))
|
||||
else: d['item'].setForeground(3, QtGui.QColor(255, 50, 50))
|
||||
except: pass
|
||||
|
||||
def get_processable_items(ui):
|
||||
valid_items = []
|
||||
for d in ui.bake_items:
|
||||
obj = rt.getNodeByName(d['name'])
|
||||
if obj: valid_items.append(d)
|
||||
return valid_items
|
||||
@ -0,0 +1,144 @@
|
||||
import os
|
||||
from pymxs import runtime as rt
|
||||
try: from PySide6 import QtWidgets, QtCore, QtGui
|
||||
except ImportError: from PySide2 import QtWidgets, QtCore, QtGui
|
||||
|
||||
import vr4life_engine as eng
|
||||
import vr4life_cloud as cld
|
||||
import importlib
|
||||
importlib.reload(eng)
|
||||
importlib.reload(cld)
|
||||
|
||||
def get_max_window_safe():
|
||||
try:
|
||||
if hasattr(rt, 'getMAXWindow'): return rt.getMAXWindow()
|
||||
except: pass
|
||||
for w in QtWidgets.QApplication.topLevelWidgets():
|
||||
if w.inherits("QMainWindow") or w.windowTitle().startswith("Autodesk 3ds Max"): return w
|
||||
return None
|
||||
|
||||
class AutoBakeManager(QtWidgets.QDialog):
|
||||
def __init__(self):
|
||||
super(AutoBakeManager, self).__init__(get_max_window_safe())
|
||||
self.setWindowTitle("VR4LIFE AUTO-BAKE V167.0 - MODULAR ENTERPRISE")
|
||||
self.resize(1050, 1380); self.bake_items = []; self._is_cancelled = False
|
||||
p = self.palette(); p.setColor(QtGui.QPalette.Window, QtGui.QColor(43, 43, 43)); self.setPalette(p); self.setAutoFillBackground(True)
|
||||
self.init_ui()
|
||||
rt.clearListener(); eng.load_bake_elements(self)
|
||||
|
||||
def init_ui(self):
|
||||
layout = QtWidgets.QVBoxLayout(self)
|
||||
lbl_t = QtWidgets.QLabel("VR4LIFE - V167.0 (Modular)"); lbl_t.setAlignment(QtCore.Qt.AlignCenter); lbl_t.setStyleSheet("font-size: 24px; font-weight: bold; color: #00FF00;")
|
||||
layout.addWidget(lbl_t)
|
||||
|
||||
self.tabs = QtWidgets.QTabWidget()
|
||||
self.tabs.setStyleSheet("QTabWidget::pane { border: 1px solid #777; background: #333; } QTabBar::tab { background: #444; color: #CCC; padding: 10px 15px; font-weight: bold; border: 1px solid #222; } QTabBar::tab:selected { background: #555; color: #00FF00; } QLabel { color: #FFF; font-weight: bold; }")
|
||||
|
||||
# ABA 1
|
||||
t_gen = QtWidgets.QWidget(); l_gen = QtWidgets.QVBoxLayout(t_gen); f_gen = QtWidgets.QFormLayout()
|
||||
self.btn_ref = QtWidgets.QPushButton("🔄 RECARREGAR MAPAS"); self.btn_ref.setStyleSheet("background: #555; color: white; height: 35px;"); self.btn_ref.clicked.connect(lambda: eng.load_bake_elements(self))
|
||||
self.cmb_bake_elem = QtWidgets.QComboBox(); self.cmb_bake_elem.setStyleSheet("background: white; color: black; font-weight: bold;")
|
||||
self.chk_denoise = QtWidgets.QCheckBox("✨ Ativar Denoiser IA"); self.chk_denoise.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_denoise.setChecked(True)
|
||||
self.edt_p_bake = QtWidgets.QLineEdit(rt.maxFilePath + "_BAKE_JPG\\" if rt.maxFilePath else "C:\\VR4_BAKE\\"); self.edt_p_bake.setStyleSheet("background: white; color: black;")
|
||||
self.edt_p_glb = QtWidgets.QLineEdit(rt.maxFilePath + "_EXPORT_GLB\\" if rt.maxFilePath else "C:\\VR4_GLB\\"); self.edt_p_glb.setStyleSheet("background: white; color: black;")
|
||||
self.btn_brw_glb = QtWidgets.QPushButton("..."); self.btn_brw_glb.setStyleSheet("background: #777; color: white; font-weight: bold;"); self.btn_brw_glb.clicked.connect(self.browse_glb_folder)
|
||||
h_glb = QtWidgets.QHBoxLayout(); h_glb.addWidget(self.edt_p_glb); h_glb.addWidget(self.btn_brw_glb)
|
||||
f_gen.addRow("Mapa:", self.cmb_bake_elem); f_gen.addRow("", self.chk_denoise); f_gen.addRow("Pasta JPG:", self.edt_p_bake); f_gen.addRow("Pasta GLB/ZIP:", h_glb)
|
||||
l_gen.addWidget(self.btn_ref); l_gen.addLayout(f_gen); l_gen.addStretch(); self.tabs.addTab(t_gen, "🗂️ 1. Geral")
|
||||
|
||||
# ABA 2
|
||||
t_geo = QtWidgets.QWidget(); l_geo = QtWidgets.QVBoxLayout(t_geo); f_geo = QtWidgets.QFormLayout()
|
||||
self.chk_a_weld = QtWidgets.QCheckBox("Fundir Grupos na opção 'AUTO'"); self.chk_a_weld.setStyleSheet("color: #00FF00; font-weight: bold;"); self.chk_a_weld.setChecked(True)
|
||||
self.chk_a_super = QtWidgets.QCheckBox("Super Solda na opção 'AUTO'"); self.chk_a_super.setStyleSheet("color: #00FFFF; font-weight: bold;"); self.chk_a_super.setChecked(False)
|
||||
self.spn_pct = QtWidgets.QDoubleSpinBox(); self.spn_pct.setStyleSheet("background: white; color: black;"); self.spn_pct.setRange(0.1, 100.0); self.spn_pct.setValue(90.0)
|
||||
self.spn_min_poly = QtWidgets.QSpinBox(); self.spn_min_poly.setStyleSheet("background: white; color: black;"); self.spn_min_poly.setRange(50, 10000000); self.spn_min_poly.setValue(3000)
|
||||
self.spn_max_sz = QtWidgets.QDoubleSpinBox(); self.spn_max_sz.setStyleSheet("background: white; color: black;"); self.spn_max_sz.setRange(10.0, 100000.0); self.spn_max_sz.setValue(800.0)
|
||||
self.chk_a_slice = QtWidgets.QCheckBox("Incluir Multi-UV no 'AUTO'"); self.chk_a_slice.setStyleSheet("color: white; font-weight: bold;")
|
||||
f_geo.addRow(QtWidgets.QLabel("🧩 SOLDADOR:")); f_geo.addRow("", self.chk_a_weld); f_geo.addRow("", self.chk_a_super)
|
||||
f_geo.addRow(QtWidgets.QLabel("🛠️ OTIMIZADOR:")); f_geo.addRow("Vertex %:", self.spn_pct); f_geo.addRow("Meta Polys:", self.spn_min_poly); f_geo.addRow(QtWidgets.QLabel("✂️ MULTI-UV:")); f_geo.addRow("Criar IDs se >:", self.spn_max_sz); f_geo.addRow("", self.chk_a_slice)
|
||||
l_geo.addLayout(f_geo); l_geo.addStretch(); self.tabs.addTab(t_geo, "📐 2. Geometria")
|
||||
|
||||
# ABA 3 e 4 mantêm-se iguais
|
||||
t_tex = QtWidgets.QWidget(); l_tex = QtWidgets.QVBoxLayout(t_tex); f_tex = QtWidgets.QFormLayout()
|
||||
self.spn_res = QtWidgets.QSpinBox(); self.spn_res.setStyleSheet("background: white; color: black;"); self.spn_res.setRange(128, 8192); self.spn_res.setValue(2048)
|
||||
self.chk_a_res = QtWidgets.QCheckBox("Auto-Size"); self.chk_a_res.setStyleSheet("color: #FFD700; font-weight: bold;"); self.chk_a_res.setChecked(True)
|
||||
self.s256 = QtWidgets.QDoubleSpinBox(); self.s256.setStyleSheet("background: white; color: black;"); self.s256.setRange(0.1, 100000.0); self.s256.setValue(100.0); self.s256.setPrefix("< 256px: ")
|
||||
self.s512 = QtWidgets.QDoubleSpinBox(); self.s512.setStyleSheet("background: white; color: black;"); self.s512.setRange(0.1, 100000.0); self.s512.setValue(300.0); self.s512.setPrefix("< 512px: ")
|
||||
self.s1024 = QtWidgets.QDoubleSpinBox(); self.s1024.setStyleSheet("background: white; color: black;"); self.s1024.setRange(0.1, 100000.0); self.s1024.setValue(600.0); self.s1024.setPrefix("< 1024px: ")
|
||||
h_sz = QtWidgets.QHBoxLayout(); h_sz.addWidget(self.chk_a_res); h_sz.addWidget(self.s256); h_sz.addWidget(self.s512); h_sz.addWidget(self.s1024)
|
||||
for w in [self.chk_a_res, self.s256, self.s512, self.s1024, self.spn_res]: w.valueChanged.connect(self.upd_res_col) if hasattr(w, 'valueChanged') else w.toggled.connect(self.upd_res_col)
|
||||
f_tex.addRow("Res Máx (>):", self.spn_res); f_tex.addRow("Escala:", h_sz)
|
||||
l_tex.addLayout(f_tex); l_tex.addStretch(); self.tabs.addTab(t_tex, "🎨 3. Textura")
|
||||
|
||||
t_cld = QtWidgets.QWidget(); l_cld = QtWidgets.QVBoxLayout(t_cld); f_cld = QtWidgets.QFormLayout()
|
||||
self.e_hash = QtWidgets.QLineEdit(); self.e_hash.setStyleSheet("background: white; color: black; letter-spacing: 2px;"); self.e_hash.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||
self.b_conn = QtWidgets.QPushButton("🔄 Conectar CMS"); self.b_conn.setStyleSheet("background: #2E8B57; color: white; height: 30px;"); self.b_conn.clicked.connect(lambda: cld.mock_connect_api(self))
|
||||
self.c_chn = QtWidgets.QComboBox(); self.c_chn.setStyleSheet("background: #CCC; color: black;"); self.c_chn.setEnabled(False)
|
||||
self.c_pst = QtWidgets.QComboBox(); self.c_pst.setStyleSheet("background: #CCC; color: black;"); self.c_pst.setEnabled(False); self.c_pst.currentIndexChanged.connect(lambda idx: cld.on_post_changed(self, idx))
|
||||
self.e_tit = QtWidgets.QLineEdit(); self.e_tit.setStyleSheet("background: #CCC; color: black;"); self.e_tit.setEnabled(False)
|
||||
self.e_mp3 = QtWidgets.QLineEdit(); self.e_mp3.setStyleSheet("background: #CCC; color: black;"); self.e_mp3.setEnabled(False)
|
||||
self.b_mp3 = QtWidgets.QPushButton("🎵 MP3"); self.b_mp3.setStyleSheet("background: #555; color: white;"); self.b_mp3.setEnabled(False); self.b_mp3.clicked.connect(self.browse_mp3)
|
||||
h_mp3 = QtWidgets.QHBoxLayout(); h_mp3.addWidget(self.e_mp3); h_mp3.addWidget(self.b_mp3)
|
||||
self.chk_tmb = QtWidgets.QCheckBox("📸 Thumbnail"); self.chk_tmb.setStyleSheet("color: gray; font-weight: bold;"); self.chk_tmb.setEnabled(False)
|
||||
self.chk_up = QtWidgets.QCheckBox("🚀 Upload Multipart Automático"); self.chk_up.setStyleSheet("color: gray; font-weight: bold;"); self.chk_up.setEnabled(False)
|
||||
f_cld.addRow("Chave:", self.e_hash); f_cld.addRow("", self.b_conn); f_cld.addRow("Canal:", self.c_chn); f_cld.addRow("Ação:", self.c_pst); f_cld.addRow("Título:", self.e_tit); f_cld.addRow("Som:", h_mp3)
|
||||
f_cld.addRow("", self.chk_tmb); f_cld.addRow("", self.chk_up)
|
||||
l_cld.addLayout(f_cld); l_cld.addStretch(); self.tabs.addTab(t_cld, "☁️ 4. API Cloud")
|
||||
layout.addWidget(self.tabs)
|
||||
|
||||
self.btn_full = QtWidgets.QPushButton("🚀 EXECUTAR TUDO (AUTO)"); self.btn_full.setStyleSheet("background: #00A8FF; color: white; font-weight: bold; font-size: 15px; height: 50px; border-radius: 8px;")
|
||||
self.btn_full.clicked.connect(self.run_full_auto); layout.addWidget(self.btn_full)
|
||||
|
||||
self.tree = QtWidgets.QTreeWidget(); self.tree.setStyleSheet("background: white; color: black;"); self.tree.setHeaderLabels(["Objeto", "Status", "Polys", "Res", "Eixo"])
|
||||
self.tree.setColumnWidth(0, 260); self.tree.setColumnWidth(1, 140); self.tree.setColumnWidth(2, 70); self.tree.setColumnWidth(3, 70); self.tree.setColumnWidth(4, 70); self.tree.itemClicked.connect(self.on_item_click)
|
||||
layout.addWidget(self.tree)
|
||||
|
||||
self.pb = QtWidgets.QProgressBar(); self.pb.setAlignment(QtCore.Qt.AlignCenter); self.pb.setStyleSheet("QProgressBar { font-weight: bold; color: black; background: white; } QProgressBar::chunk { background: #00FF00; }"); self.pb.setFormat("Pronto")
|
||||
layout.addWidget(self.pb)
|
||||
|
||||
h_l = QtWidgets.QHBoxLayout()
|
||||
self.b1 = QtWidgets.QPushButton("P1: Lista"); self.b1.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b1.clicked.connect(lambda: eng.load_selection(self))
|
||||
self.b2 = QtWidgets.QPushButton("P2: Fundir"); self.b2.setStyleSheet("background: #32CD32; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2.clicked.connect(lambda: eng.attach_grouped_objects(self, False))
|
||||
self.b2_5 = QtWidgets.QPushButton("P2.5: Super Solda"); self.b2_5.setStyleSheet("background: #008080; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b2_5.clicked.connect(lambda: eng.super_attach_objects(self, False))
|
||||
self.b3 = QtWidgets.QPushButton("P3: Opt"); self.b3.setStyleSheet("background: #FF8C00; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b3.clicked.connect(lambda: eng.optimize_geometry(self))
|
||||
|
||||
self.b4 = QtWidgets.QPushButton("P4: Multi-UV"); self.b4.setStyleSheet("background: #8B008B; color: white; font-weight: bold; height: 35px; border-radius: 4px;"); self.b4.clicked.connect(lambda: eng.slice_large_objects(self, False))
|
||||
|
||||
self.b5 = QtWidgets.QPushButton("P5: UV"); self.b5.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b5.clicked.connect(lambda: eng.prepare_mesh(self))
|
||||
self.b6 = QtWidgets.QPushButton("P6: Bake"); self.b6.setStyleSheet("background: #E0E0E0; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b6.clicked.connect(lambda: eng.process_bake_logic(self, False))
|
||||
self.b7 = QtWidgets.QPushButton("P7: Exportar"); self.b7.setStyleSheet("background: #FFD700; color: black; font-weight: bold; height: 35px; border-radius: 4px;"); self.b7.clicked.connect(lambda: cld.finalize_export(self, self.edt_p_bake.text(), self.edt_p_glb.text()))
|
||||
|
||||
for b in [self.b1, self.b2, self.b2_5, self.b3, self.b4, self.b5, self.b6, self.b7]: h_l.addWidget(b)
|
||||
layout.addLayout(h_l)
|
||||
|
||||
self.btn_cancel = QtWidgets.QPushButton("CANCELAR / FECHAR"); self.btn_cancel.setStyleSheet("background: #FF0000; color: white; font-weight: bold; height: 40px; border-radius: 8px;"); self.btn_cancel.clicked.connect(self.cancel_all)
|
||||
layout.addWidget(self.btn_cancel)
|
||||
|
||||
def browse_glb_folder(self):
|
||||
f = QtWidgets.QFileDialog.getExistingDirectory(self, "Selecione a Pasta GLB")
|
||||
if f: self.edt_p_glb.setText(f + "\\")
|
||||
|
||||
def browse_mp3(self):
|
||||
f, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Selecione o Áudio", "", "MP3 (*.mp3)")
|
||||
if f: self.e_mp3.setText(f); self.e_mp3.setStyleSheet("background: white; color: green; font-weight: bold;")
|
||||
|
||||
def upd_res_col(self): eng.upd_res_col(self)
|
||||
def get_processable_items(self): return eng.get_processable_items(self)
|
||||
|
||||
def on_item_click(self, item, column):
|
||||
o = rt.getNodeByName(item.text(0))
|
||||
if o: rt.select(o); rt.redrawViews()
|
||||
|
||||
def run_full_auto(self):
|
||||
self._is_cancelled = False
|
||||
if not rt.execute("selection as array"): QtWidgets.QMessageBox.warning(self, "Aviso", "Selecione algo!"); return
|
||||
if self.chk_a_weld.isChecked() and not self._is_cancelled: eng.attach_grouped_objects(self, True)
|
||||
if self.chk_a_super.isChecked() and not self._is_cancelled: eng.super_attach_objects(self, True)
|
||||
eng.load_selection(self)
|
||||
if not self.get_processable_items(): return
|
||||
if self.bake_items and not self._is_cancelled: eng.optimize_geometry(self)
|
||||
if self.chk_a_slice.isChecked() and not self._is_cancelled: eng.slice_large_objects(self, True)
|
||||
if not self._is_cancelled: eng.prepare_mesh(self)
|
||||
if not self._is_cancelled: time.sleep(1.0); eng.process_bake_logic(self, True)
|
||||
|
||||
def cancel_all(self): self._is_cancelled = True; self.close()
|
||||
@ -0,0 +1,92 @@
|
||||
import os
|
||||
import urllib.request
|
||||
from pymxs import runtime as rt
|
||||
try: from PySide6 import QtWidgets
|
||||
except ImportError: from PySide2 import QtWidgets
|
||||
|
||||
# URL Oficial do repositório da Immerse Games (Lendo a branch 'main')
|
||||
# Obs: Se o seu Gitea estiver usando 'master' em vez de 'main', basta trocar a última palavra.
|
||||
GITEA_RAW_URL = "https://git.immersegame.com/immersegame/vr4life-3dmax-plugin/raw/branch/main/"
|
||||
|
||||
# Token de Leitura do Gitea
|
||||
GITEA_TOKEN = "efebcde14ce96a2b80d0b3f207991bc155018ab8"
|
||||
|
||||
FILES_TO_UPDATE = [
|
||||
"vr4life_ui.py",
|
||||
"vr4life_engine.py",
|
||||
"vr4life_cloud.py",
|
||||
"run_vr4life.py",
|
||||
"vr4life_updater.py"
|
||||
]
|
||||
|
||||
def check_and_update(ui_parent=None):
|
||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
local_version_file = os.path.join(script_dir, "version.txt").replace("\\", "/")
|
||||
|
||||
if ui_parent:
|
||||
ui_parent.pb.setFormat("Autenticando e checando versão no Gitea..."); QtWidgets.QApplication.processEvents()
|
||||
|
||||
try:
|
||||
# 1. Lê a versão local (Se não existir, assume 0.0)
|
||||
local_version = "0.0"
|
||||
if os.path.exists(local_version_file):
|
||||
with open(local_version_file, "r") as f:
|
||||
local_version = f.read().strip()
|
||||
|
||||
# 2. Bate na Nuvem com o Token para ler o version.txt
|
||||
remote_version_url = GITEA_RAW_URL + "version.txt"
|
||||
req_version = urllib.request.Request(remote_version_url)
|
||||
req_version.add_header("Authorization", f"token {GITEA_TOKEN}")
|
||||
|
||||
response_version = urllib.request.urlopen(req_version)
|
||||
remote_version = response_version.read().decode('utf-8').strip()
|
||||
|
||||
# 3. Compara as versões: Só baixa se a do Gitea for mais nova
|
||||
if remote_version == local_version:
|
||||
if ui_parent:
|
||||
ui_parent.pb.setFormat("Sistema já está atualizado!"); ui_parent.pb.setValue(100)
|
||||
QtWidgets.QMessageBox.information(ui_parent, "Atualizador", f"Você já está usando a versão mais recente ({local_version}).")
|
||||
return
|
||||
|
||||
# 4. Inicia o Download Seguro dos Arquivos
|
||||
if ui_parent:
|
||||
ui_parent.pb.setFormat(f"Nova versão {remote_version} encontrada! Baixando..."); QtWidgets.QApplication.processEvents()
|
||||
|
||||
updated_count = 0
|
||||
for file_name in FILES_TO_UPDATE:
|
||||
remote_url = GITEA_RAW_URL + file_name
|
||||
local_path = os.path.join(script_dir, file_name).replace("\\", "/")
|
||||
|
||||
if ui_parent:
|
||||
ui_parent.pb.setFormat(f"Baixando {file_name}..."); QtWidgets.QApplication.processEvents()
|
||||
|
||||
# Puxa o código com o Token
|
||||
req = urllib.request.Request(remote_url)
|
||||
req.add_header("Authorization", f"token {GITEA_TOKEN}")
|
||||
response = urllib.request.urlopen(req)
|
||||
remote_code = response.read().decode('utf-8')
|
||||
|
||||
# Sobrescreve na máquina local
|
||||
with open(local_path, "w", encoding="utf-8") as f:
|
||||
f.write(remote_code)
|
||||
|
||||
updated_count += 1
|
||||
|
||||
# 5. Salva a nova versão local para a próxima checagem
|
||||
with open(local_version_file, "w") as f:
|
||||
f.write(remote_version)
|
||||
|
||||
msg = f"Sucesso! Plugin atualizado para a versão {remote_version} ({updated_count} arquivos).\n\nPor favor, feche esta janela e abra o plugin novamente pelo menu superior."
|
||||
if ui_parent:
|
||||
ui_parent.pb.setFormat("Atualização Concluída!"); ui_parent.pb.setValue(100)
|
||||
QtWidgets.QMessageBox.information(ui_parent, "Update VR4Life", msg)
|
||||
else:
|
||||
rt.messageBox(msg, title="Update VR4Life")
|
||||
|
||||
except Exception as e:
|
||||
erro_msg = f"Falha de Autenticação ou Download.\nVerifique seu Token e URL do Gitea.\n\nDetalhe técnico: {str(e)}"
|
||||
if ui_parent:
|
||||
QtWidgets.QMessageBox.critical(ui_parent, "Erro de Update", erro_msg)
|
||||
ui_parent.pb.setFormat("Pronto"); ui_parent.pb.setValue(0)
|
||||
else:
|
||||
rt.messageBox(erro_msg, title="Erro de Update")
|
||||
@ -0,0 +1,45 @@
|
||||
macroScript Launch_VR4Life
|
||||
category:"VR4Life"
|
||||
tooltip:"VR4Life Auto-Bake Plugin"
|
||||
buttonText:"Ligar VR4Life"
|
||||
(
|
||||
on execute do (
|
||||
python.ExecuteFile @"Z:\Emmersive games\3dxMAX\vr4life-3dmax-plugin\run_vr4life.py"
|
||||
)
|
||||
)
|
||||
|
||||
macroScript Reload_VR4Life
|
||||
category:"VR4Life"
|
||||
tooltip:"Salvar Estado e Recarregar Plugin"
|
||||
buttonText:"Recarregar VR4Life"
|
||||
(
|
||||
on execute do (
|
||||
python.Execute "import pymxs\ntry:\n pymxs.runtime.vr4_app.reload_plugin()\nexcept:\n pymxs.runtime.python.ExecuteFile(r'Z:\\Emmersive games\\3dxMAX\\vr4life-3dmax-plugin\\run_vr4life.py')"
|
||||
)
|
||||
)
|
||||
|
||||
(
|
||||
local mainMenu = menuMan.getMainMenuBar()
|
||||
|
||||
local oldMenu = menuMan.findMenu "VR4Life"
|
||||
if oldMenu != undefined do (
|
||||
menuMan.unRegisterMenu oldMenu
|
||||
)
|
||||
|
||||
local vr4Menu = menuMan.createMenu "VR4Life"
|
||||
|
||||
local launchItem = menuMan.createActionItem "Launch_VR4Life" "VR4Life"
|
||||
vr4Menu.addItem launchItem -1
|
||||
|
||||
local sep = menuMan.createSeparatorItem()
|
||||
vr4Menu.addItem sep -1
|
||||
|
||||
local reloadItem = menuMan.createActionItem "Reload_VR4Life" "VR4Life"
|
||||
vr4Menu.addItem reloadItem -1
|
||||
|
||||
local topLevelItem = menuMan.createSubMenuItem "VR4Life" vr4Menu
|
||||
mainMenu.addItem topLevelItem -1
|
||||
|
||||
menuMan.updateMenuBar()
|
||||
messageBox "Menu VR4Life atualizado! Agora a opcao de Recarregar foi transferida para ca." title:"VR4Life Toolbar"
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1 @@
|
||||
[{"name": "ETICHETTA", "status": "DONE V19", "polys": "5,966", "res": "256px", "id": "60.0"}, {"name": "Object003", "status": "DONE V19", "polys": "7,470", "res": "1024px", "id": "2394.0"}, {"name": "ETICHE_005", "status": "DONE V19", "polys": "2,977", "res": "256px", "id": "56.0"}, {"name": "IMBOTT_002", "status": "DONE V19", "polys": "6,628", "res": "1024px", "id": "2751.4"}, {"name": "Object005", "status": "DONE V19", "polys": "2,846", "res": "1024px", "id": "2456.7"}, {"name": "Object004", "status": "DONE V19", "polys": "2,962", "res": "1024px", "id": "1377.5"}, {"name": "Object006", "status": "DONE V19", "polys": "2,878", "res": "1024px", "id": "764.3"}, {"name": "CUCITURE", "status": "DONE V19", "polys": "936", "res": "256px", "id": "65.8"}]
|
||||
Loading…
Reference in New Issue