├── README.md ├── fast_pbr_viewport_render ├── FastPBRAssets.blend ├── __init__.py └── fileRenamer.py └── sword.mp4 /README.md: -------------------------------------------------------------------------------- 1 | 4 | ![fastPBRShortBanner](https://user-images.githubusercontent.com/20190653/143673710-58680963-e937-478d-8d06-dcc9cdacddd2.jpg) 5 | 6 | # FastPBR 7 | 8 | Fast PBR Viewport Render is a Blender addon that lets you fetch curvature, AO, normal maps, transparency, matID and height from the camera youre currently looking through or directly from your viewport. It uses Blenders "Render viewport" operator which renders pretty much exactly what you see on screen or what the camera you are looking through currently has within frame. Therefore its extremely flexible as "what you see is what you get" and as you can use it ANYWHERE to render ANY sort of geometry or scene. It works by modifying your render settings and renderer (it uses Eevee and workbench for maximum performance), it can render out several 4K maps in the matter of seconds, making it versatile and useable for projects where you need to do a lot of rendering (and hence cant afford any time lost for waiting). It also comes equipped with a flexible, configurable and automatic file naming and folder hierarchy system that lets you move and name your images wherever you want based on custom & automatically generated variables that describes your images. 9 | 10 | *Note: The readme is somewhat outdated as the addon has a lot more features (unrelated to just texture creation) at this date <3 Possible addon rename also coming up as it can do many more things now outside of generating PBR images. Also note that the source code here on git is somewhat out of date (this is because some of the files in this repo are too large for github), instead download the latest release that you can see under "Releases" to the right. The addon is actively maintained & in a development phase, more documentation and explanations of how to use all the tools on a more detailed level will come once a larger portion of the addons features are at a mature & well-tested state.* 11 | # Discord 12 | 13 | Join our dedicated Discord server! 14 | 18 | https://discord.gg/TD6qNA9y7H 19 | 20 | [![Welcome!](https://user-images.githubusercontent.com/20190653/160453732-f0d2b6e5-d4ae-4256-9510-43e2e5c88c84.png)](https://discord.gg/TD6qNA9y7H) 21 | 22 | # Limitations 23 | * Runs on Windows based devices exclusively at the moment. If anyone would be interested in making it work cross platform I would gladly accept any collaboration! 24 | * Can only bake from a single perspective (of which you choose) as the addon uses Eevee and workbench to perform the bakes. In other words, you cant bake using a target mesh based on another mesh's UV. 25 | # Features 26 | * Bake any visible geometry, could be curves, text, objects w modifiers (etc). 27 | * Supports baking geometry that has Z fighting (the Z fighting wont show in the renders) as well as geometry with inverted normals (the addon tries to automatically detect inverted normals and corrects them once found through a fast post process filter). 28 | * Capable of baking/rendering multiple cameras, each camera rendering multiple passes with the click of a single button using customizeable predefined configurations & output image naming. 29 | * Advanced highly customizeable file naming & folder hierarchy system that you can use to process images emitted by either FastPRR or by any external software. It lets you fetch data like the image resolution, folder name, file type (etc) and insert such data anywhere in the path that you want to export your images to or the images names themselves. - Its also really easy to use. The intended usecase for this is to let the addon to be your backbone for organizing your files in a reliable way, rather than you renaming your images and moving them to the correct folders manually. 30 | * Experimental (v1.3 and forward) : Mesh processing features that can convert most types of geometry (like curves, text (etc)) to raw mesh. This has very little to do with PBR however :P , but is useful for when preparing your geometry for export using something like FBX or glTF. It uses a philosophy of that everything should look the same after processing, but converts everything to raw MESH based objects. 31 | * Experimental (v1.3 and forward) : Material merger, capable of merging materials that are identical, but that do not use "shared data" (this is achieved using value comparing for all the nodes in the node graph as well as all the material settings to see if the node graph, material and all the associated settings have the same values). The motivation for this feature was to merge duplicate materials generated by DecalMachin3 as it can generate thousands of identical materials in larger scenes (therefore the addon has some dedicated code geared specifically towards merging these decalMachin3 materials), however the feature is generic and works for all sorts of materials. 32 | * Match viewport display and Surface Base Color. Simply put takes whatever color is set under Material > Surface > Base Color and copies it over to Material > Viewport Display > Color. This is useful when wanting your materials albedo to match when previewing them in both workbench and Eevee/Cycles. 33 | ## Supported bake passes: 34 | * Diffuse (color + lighting, gives you what you see when previewing in EEVEE or Cycles) 35 | * Albedo (color) - useable for Mat ID 36 | * normal 37 | * Curvature 38 | * Ambient Occlusion (AO using Eevee) 39 | * Height (uses Eevee's mist pas making the BW map limited to 256 possible values per pixel, however the output is linear like its supposed to) 40 | * Transparency (makes the world background black & anything obscuring it white) 41 | * MatID 42 | 43 | # Video demos 44 | 60 | 61 | 62 | * Simple bake setup: https://user-images.githubusercontent.com/20190653/136704470-e3d3a362-9d9f-48e7-8a6a-69f4d155d6db.mp4 63 | The resulting image created in the video ended up looking like so once combined with some smart materials in "Quixel Mixer" afterwards: https://user-images.githubusercontent.com/20190653/136704479-869414bf-1a4b-440c-bd17-36b90903a5ad.jpg 64 | 65 | * A quick usage example showcasing how to fetch heightmap from a camera in Blender, extracting the height data & then using that to create a landscape in Unreal Engine 5: 66 | https://youtu.be/kGkdHABi6yE 67 | * Trimsheet unwrapping: https://youtu.be/CXOazuUQ_3Y 68 | 69 | * 1.7.0 Turntable render: [https://youtu.be/nlrJ_2AIcQc](https://youtu.be/nlrJ_2AIcQc) 70 | 71 | # Install instructions 72 | 73 | *If you prefer a video guide: https://youtu.be/wev76s-1FG0* 74 | 75 | 1. [Download](https://github.com/ItsCubeTime/FastPBR/releases) and install the addon .zip file like usual (via Top Panel > Edit > Preferences > Add-ons > Install > select the zip). 76 | 77 | 2. As of v1.7.0, after the initial activation of the addon you should now see an error message asking you to restart Blender to complete the installation. If you see this, simply restart Blender and activate the addon once more. 78 | 79 | 3. You should now see the dedicated "Fast PBR Viewpot Render" panel in In "your 3D viewport > Left side Bar/foldout panel (N is the default hotkey to open/close) > Fast > Fast PBR". 80 | 81 | 4. Optional step if your normal matcap doesnt install automatically: Download the FastPBRNormalMatCap.exr attached in any of the [releases](https://github.com/ItsCubeTime/FastPBR/releases) (as of 1.2.1 and up the exr file is bundled as a part of the zip file for convenience) and install it as a matCap in Blender (& make sure you dont rename it, the script utelizes the matcap to generate the normal maps): 82 | ![image](https://user-images.githubusercontent.com/20190653/136703921-1df20cef-71d8-4ca2-af3f-72d43ecad2f3.png) 83 | 84 | # Usage 85 | 86 | 1. Choose a destination path, note that this path has to include the file name itself and the file extension. Only png is supposed at the time being, so make sure that it ends with png. 87 | 2. Configure the render passes you would like to use by toggling the checkboxes, more settings might become available in there foldout panels on a later time. 88 | 3. Choose the resolution by setting your camera resolution like usual in Properties > Output Properties. 89 | 4. Save your file for safety (The addon modifies your render settings and later reverts them, but if the script fails to reach the end due to a fatal error (which could occur if the Blender version you're using is not 100% compatable) it might not get a chance to restore the settings. If that happens however, theres a "restore settings" operator/button that you can call/press to revert the settings yourself as the addon automatically sets up a restore state to go back to. 90 | 5. Hit the top button "Fast PBR Viewport Render" to render out your maps, once the operation is complete you will see a pop-up message appear at your cursor as well as that Windows file explorer will automatically open up the directory that the images has been created in for you. 91 | 6. To render multiple cameras at once, name the cameras as you lik 92 | 93 | To find out more about each button and property in the UI, you can hover over it to get a short decsription: 94 | ![image](https://user-images.githubusercontent.com/20190653/136705600-442c0aa1-3f48-4595-860d-9d6a7ec426e6.png) 95 | 96 | # Overview 97 | ![image](https://user-images.githubusercontent.com/20190653/143656380-2e4c28b8-c64d-47ba-bd23-d3e613b3d0ec.png) 98 | # Contributing 99 | 100 | Anyone is welcome to contribute, the addon is 100% pure Python 3. 101 | 102 | # Troubleshooting 103 | 104 | If you encounter any issues, dont hesitate to open up an issue ticket here on Github and I will do my best to assist. <3 Also dont shy away from digging in the code yourself if you believe you can figure out whats happening in the event of an unexpected Python error or any sort of unexpected behaviour. 105 | 106 | Also make sure to read the below section "Known issues" 107 | 108 | # Known issues 109 | 110 | 1. (Now fixed) The transparency pass is inverted. 111 | 2. (Now fixed as of 1.3) The height doesnt utelize the full 0-1 range as its utelizing Eevees mist- 112 | 3. (Now fixed as of 1.8.0) In the 1.7.0 release the addon seems to not successfully activate the first time, activating it twice will have it start properly. I will try adressing this in the future 113 | 114 | # Things I want to add 115 | 116 | 1. Support for normal maps that are texture/shader based. 117 | 2. (Now implemented, although not error free) Support for baking geometry with inverted normals without it affecting the result of the baked texture emitted by Fast PBR. 118 | 3. Better height maps. 119 | 4. A grid visualizer that lets you preview how large a pixel will be on the final image before performing a render. 120 | 5. Bake passes for material/shader based roughness/specular/metallic. 121 | 6. Got a sick idea? Dont hesitate to let me know about it! 122 | 123 | # Contact 124 | 125 | You can reach me by either opening an issue or discussion here on Github, or sending a dm to me over Discord: danieljackson#0286 or saying hi in our Discord https://discord.gg/TD6qNA9y7H ! 126 | 127 | # Intended workflow 128 | 129 | The addon had its initial development in the process of the making of a video game "We Might Die" (an upcoming title as of writing this), we needed A LOT of textures baked from geometry for hard surface details for sci-fi ship interiors. The workflow is that you model/sculpt/generate geometry of any kind (the addon supports any sort of geometry thats visible in workbench & Eevee) in Blender or your software of choice, generate the maps and then add image based microdetails and materials in a texturing software of choice, such as Quixel Mixer. 130 | 131 | The addon is also capable of renaming images emitted by another software and doing the naming for you with naming standards of choice. 132 | 133 | Its recommended that you keep your images to the power of 2 (so 512, 1024, 2048, 4096 (etc)) and perfectly square as that will yield best performance when using it in most modern renderers as of today. 134 | 135 | *Tags: Fast Toolbox, Fast Tool box, Fast Tool-box, Fast-PBR, FastPBR, Blender, batch render, turn tables, UV unwrapping, grid UV unwrapping, Fast Blender addon, Blender script, Material Toolkit, automatic rendering, automation* 136 | 137 | ## Thanks for checking out my repo! 138 | 139 | ![fastPBRShortBanner](https://c.tenor.com/ztEJgrjFe54AAAAC/hug-anime.gif) 140 | -------------------------------------------------------------------------------- /fast_pbr_viewport_render/FastPBRAssets.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsCubeTime/FastPBR/2f824a62b48d9a805e4c2589876138818369a81c/fast_pbr_viewport_render/FastPBRAssets.blend -------------------------------------------------------------------------------- /fast_pbr_viewport_render/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | bl_info = { 4 | "name": "Fast PBR Viewport Render", 5 | "blender": (3, 00, 0), 6 | "category": "Object", 7 | } 8 | __name__ # This is the name of the folder that the __init__.py is in. I think 9 | addonName: str = bl_info["name"] 10 | addonNameShort: str = "Fast PBR" 11 | 12 | from typing import Any, Mapping, ValuesView 13 | from typing import TYPE_CHECKING 14 | 15 | from PIL import Image, ImageChops 16 | 17 | import bpy 18 | 19 | import bpy 20 | import subprocess 21 | import sys 22 | import ctypes 23 | from bpy.types import MaterialSlot, PropertyGroup 24 | # print("LOOK HERE ", sys.executable) 25 | import os 26 | import importlib 27 | def putTextInBox(text): 28 | lines = text.splitlines() 29 | width = max(len(s) for s in lines) 30 | res = ['┌' + '─' * width + '┐'] 31 | for s in lines: 32 | res.append('│' + (s + ' ' * width)[:width] + '│') 33 | res.append('└' + '─' * width + '┘') 34 | return '\n'.join(res) 35 | import pip 36 | def installPackage(package): 37 | if hasattr(pip, 'main'): 38 | pip.main(['install', package]) 39 | else: 40 | pip._internal.main(['install', package]) 41 | # def installPackage(package: str): 42 | # # subprocess.check_call(['"' + sys.executable + '"', "-m", "pip", "install", package]) 43 | # # os.system(f'"{sys.executable}" -m pip install {package}') 44 | # ctypes.windll.shell32.ShellExecuteW(None, "runas", sys.executable, f"""-c import pdb; pdb.set_trace()\nimport os\nos.system('"{sys.executable}" -m pip install {package}')\ninput()""", None, 1) 45 | 46 | # Running installPackage is a slow operation, therefore we want to make sure that we only run it when necessary, hence this function to speed up registration/activation time of the addon SIGNIFICANTLY (like by 5-10 seconds). 47 | def attemptToImportModuleAndInstallItIfItIfTheCorespondingPackageDoesntExist(packageName, moduleName): 48 | print("Attempting") 49 | try: 50 | importlib.import_module(moduleName) 51 | # from PIL import Image 52 | except Exception as error: 53 | print(putTextInBox(f"{addonName}Error: ---\n{error}\n---\nwhen attempting to import {moduleName}, we're assuming that you dont have {packageName} installed and will try to install it for you!")) 54 | installPackage(packageName) 55 | importlib.import_module(moduleName) # Doesnt actually work? 56 | ListOfModulesToAttemptToImportAndInstallItIfItIfTheCorespondingPackageDoesntExist = [['PILLOW', 'PIL'], ['numpy']] 57 | attemptToImportModuleAndInstallItIfItIfTheCorespondingPackageDoesntExist('PILLOW', 'PIL') 58 | import PIL 59 | attemptToImportModuleAndInstallItIfItIfTheCorespondingPackageDoesntExist('numpy', 'numpy') 60 | import numpy 61 | 62 | 63 | from PIL import ImageChops 64 | 65 | 66 | 67 | def copyModifiers(copyAllModifiersAndThereSettingsFromObject: bpy.types.Object, copyTargetObjects: list(bpy.types.Object)): 68 | # copyAllModifiersAndThereSettingsFromObject = bpy.context.object 69 | # copyTargetObjects = [o for o in bpy.context.selected_objects 70 | # if o != copyAllModifiersAndThereSettingsFromObject and o.type == copyAllModifiersAndThereSettingsFromObject.type] 71 | 72 | for obj in copyTargetObjects: 73 | obj: bpy.types.Object 74 | # if hasattr(obj, 'name') and hasattr(copyAllModifiersAndThereSettingsFromObject, 'name') and hasattr(obj, 'material_slots') and hasattr(obj, 'modifiers'): 75 | if obj.type == 'MESH' and not obj == copyAllModifiersAndThereSettingsFromObject: 76 | # if obj.type == copyAllModifiersAndThereSettingsFromObject.type: 77 | # if obj.type == copyAllModifiersAndThereSettingsFromObject.type: 78 | 79 | for mSrc in copyAllModifiersAndThereSettingsFromObject.modifiers: 80 | mSrc: bpy.types.Modifier 81 | mDst: bpy.types.Modifier = obj.modifiers.get(mSrc.name, None) 82 | # if not mDst: 83 | # mDst = obj.modifiers.new(mSrc.name, mSrc.type) # The if block is useful if you want to avoid duplicates, although 84 | # this can also be a bad behaviour as if the user happen to have put an identical modifier somewhere in the middle of the stack 85 | # , the script will not add another one at the end of the stack - which in our case is often necessary. 86 | # print(f"@@@@@@@@@@@@@@@@@@@@@@@@@@@@ obj: {obj}") 87 | # print(f"@@@@@@@@@@@@@@@@@@@@@@@@@@@@ mSrc: {mSrc}") 88 | # print(f"@@@@@@@@@@@@@@@@@@@@@@@@@@@@ mSrc.name: {mSrc.name}") 89 | # print(f"@@@@@@@@@@@@@@@@@@@@@@@@@@@@ mSrc.type: {mSrc.type}") 90 | mDst = obj.modifiers.new(mSrc.name, mSrc.type) 91 | # print(f"@@@@@@@@@@@@@@@@@@@@@@@@@@@@ mDst: {mDst}") 92 | # collect names of writable properties 93 | properties = [p.identifier for p in mSrc.bl_rna.properties 94 | if not p.is_readonly] 95 | 96 | # copy those properties 97 | for prop in properties: 98 | # getattr(mSrc, prop) 99 | setattr(mDst, prop, getattr(mSrc, prop)) 100 | 101 | # installPackage('PILLOW') 102 | # installPackage('numpy') 103 | 104 | 105 | 106 | # import pi 107 | # attemptToImportModuleAndInstallItIfItIfTheCorespondingPackageDoesntExist('PILLOW', 'PIL') 108 | # attemptToImportModuleAndInstallItIfItIfTheCorespondingPackageDoesntExist('numpy', 'numpy') 109 | # from PIL import Images 110 | # if TYPE_CHECKING: # Importing packages just for intellisense as our import function wont run through VS Codes intellisense engine. 111 | 112 | # import numpy 113 | # print(help(numpy)) 114 | 115 | 116 | # materialToReplaceTo = "" 117 | # materialToReplaceFrom = "" 118 | 119 | # bpy.ops.ed.undo_push() 120 | 121 | # for object in bpy.context.scene.objects: 122 | # object: bpy.types.Object 123 | # for materialSlot in object.material_slots: 124 | # materialSlot: bpy.types.MaterialSlot 125 | # if materialSlot.material.name_full == materialToReplaceFrom: 126 | # materialSlot.material = bpy.data.materials.get(materialToReplaceTo) 127 | 128 | 129 | 130 | # from "C:/Program Files/Blender Foundation/blender-3.0.0-alpha+master.2b64b4d90d67-windows.amd64-release/3.0/scripts/addons/fast_pbr_viewport_render/fileRenamer.py" import moveImages 131 | import os 132 | from .fileRenamer import * 133 | import glob 134 | 135 | 136 | pathToStoreImagesIn = "C:/FromFastPBR/" 137 | 138 | 139 | 140 | classesToRegister = list() 141 | 142 | settingsPropertyGroupParents = list() 143 | 144 | renderPasses = list() 145 | 146 | nameOfWorldUsedDuringPBRmapCreation = "FastPBRViewportRender" 147 | 148 | disableRestore = True # Set to true for debugging 149 | 150 | # addonNameShort = "Fast PBR" 151 | import pathlib 152 | pathToAddonDirectory = str(pathlib.Path(__file__).parent.resolve()) 153 | nameOfAssetsFileWithoutPath = 'FastPBRAssets.blend' 154 | pathToAssetsFile = pathToAddonDirectory + '/' + nameOfAssetsFileWithoutPath 155 | 156 | # Excludes a final / or \. Uses forward slashes 157 | pathToBlenderExeDirectoryExcludingTheExeFile: str = str(bpy.app.binary_path) 158 | # pathToBlenderExeDirectoryExcludingTheExeFile = pathToBlenderExeDirectoryExcludingTheExeFile.split(pathToBlenderExeDirectoryExcludingTheExeFile.rfind("\\")) 159 | pathToBlenderExeDirectoryExcludingTheExeFile = pathToBlenderExeDirectoryExcludingTheExeFile.replace("\\", "/") 160 | pathToBlenderExeDirectoryExcludingTheExeFile = pathToBlenderExeDirectoryExcludingTheExeFile[0:pathToBlenderExeDirectoryExcludingTheExeFile.rfind('/')] 161 | print("pathToBlenderExeDirectoryExcludingTheExeFile: " + pathToBlenderExeDirectoryExcludingTheExeFile) 162 | 163 | 164 | 165 | 166 | ############################# 167 | ##### Utility functions ##### 168 | ############################# 169 | # @bookmark Utility functions 170 | def ShowPopupMessageBoxAtCursor(message = "", title = "Message Box", icon = 'INFO'): 171 | 172 | def draw(self, context): 173 | self.layout.label(text=message) 174 | 175 | print(message) 176 | return {'FINISHED'} 177 | 178 | bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) 179 | 180 | 181 | def appendAssetFromAssetsBlendFile(dataBlockToAppend: str, blendFileDataCategory: str): 182 | """ 183 | dataBlockToAppend is the name of the object/material/whatever you want to append. 184 | blendFileDataCategory is the type of data you want to append, if you go into the outliner > display mode > Data API you will see all these categories. Example values: "Material", "Object", "Node Groups" (etc). For some reason, the "s" at the end of some categories displayed in that list is not supposed to be included, which is rather confusing :) 185 | """ 186 | bpy.ops.wm.append(filename=dataBlockToAppend, directory=pathToAssetsFile + '\\' + blendFileDataCategory + '\\') 187 | 188 | print("filepath:", nameOfAssetsFileWithoutPath) 189 | print("directory:", pathToAssetsFile + '\\' + blendFileDataCategory) 190 | print("filename:", dataBlockToAppend) 191 | 192 | def containsLowerCase(string): 193 | # string: str = "" 194 | for letter in string: 195 | if letter.islower(): 196 | # print("aaa") 197 | return True 198 | return False 199 | 200 | def PascalCaseTo_snake_case(string): 201 | # string = 202 | 203 | 204 | string: str = 'A' + string.replace(" ", "_") + 'A' 205 | outputString: str = string.lower() 206 | 207 | firstRun = True 208 | for index in range(string.__len__()-1, -1, -1): 209 | letter = string[index] 210 | if not firstRun and index is not 1: 211 | nextLetter = string[index+1] 212 | previousLetter = string[index-1] 213 | if letter.isupper() and (nextLetter.islower() or previousLetter.islower()) and (index is not 0) and containsLowerCase(string[:index]): 214 | if not '_' in [letter, nextLetter, previousLetter] and not '.' in [letter, nextLetter, previousLetter]: 215 | outputString = outputString[:index] + "_" + outputString[index:] 216 | # print(index) 217 | else: 218 | firstRun = False 219 | 220 | 221 | 222 | outputString = outputString[1:-1] 223 | 224 | return outputString 225 | 226 | # Takes something like: "FFFFFFast PBRViewportRenderRRRRRrRRR" and outputs: "FFFFFFast PBR Viewport Render RRRR Rr RRR" 227 | # Doesnt generate double spaces if a space comes before a capital letter in the input string. 228 | def insertSpaceAfterCapital(string): 229 | string: str = str(string) 230 | outputString = string 231 | for index in range(string.__len__()-1, -1, -1): 232 | letter = string[index] 233 | 234 | if letter.isupper(): 235 | if index != 0 and index != string.__len__()-1: 236 | nextLetter = string[index+1] 237 | previousLetter = string[index-1] 238 | if previousLetter.islower() or nextLetter.islower() and containsLowerCase(string[:index]): 239 | outputString = outputString[:index] + " " + outputString[index:] 240 | 241 | return outputString 242 | 243 | 244 | 245 | ############################ 246 | # End of Utility functions # 247 | ############################ 248 | 249 | 250 | 251 | #################### 252 | #### DECORATORS #### 253 | #################### 254 | # @bookmark decorators 255 | 256 | def registerClassGivebl_labelAndbl_idnameWithUnderscore(cls): 257 | cls.bl_idname = PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__) 258 | cls.bl_label = insertSpaceAfterCapital(cls.__name__) 259 | classesToRegister.append(cls) 260 | return cls 261 | 262 | def registerClassGivebl_labelAndbl_idnameWithDot(cls): 263 | cls.bl_idname = PascalCaseTo_snake_case(addonNameShort + "." + cls.__name__) 264 | cls.bl_label = insertSpaceAfterCapital(cls.__name__) 265 | classesToRegister.append(cls) 266 | return cls 267 | 268 | def settingsContainerDecorator(cls): 269 | # settingsPropertyGroupParents.append(cls) 270 | # # classesToRegister.append(cls.settings) 271 | # cls.settings = registerClassGivebl_labelAndbl_idnameWithUnderscore(cls.settings) 272 | # settingsPropertyGroupParents.append(cls) 273 | 274 | 275 | 276 | 277 | cls.settings.bl_label = insertSpaceAfterCapital(cls.__name__) 278 | cls.settings.bl_idname = PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__) 279 | cls.bl_label = insertSpaceAfterCapital(cls.__name__) 280 | cls.bl_idname = PascalCaseTo_snake_case(cls.__name__) 281 | classesToRegister.append(cls.settings) 282 | # classesToRegister.append(cls) 283 | settingsPropertyGroupParents.append(cls) 284 | return cls 285 | 286 | def renderPassDecorator(cls): 287 | # class NewClass(cls): 288 | # # print("helloFromRenderPassDecorator:", str(cls)) # For some reason, prints cant take place before the addon has been registered 289 | # # If one uncomments the print, it wont ever show up in Blenders terminal 290 | # renderPasses.append(cls) 291 | # print(f"Awesomesauce: cls.__name__") 292 | # classesToRegister.append(cls.settings) 293 | 294 | # return NewClass 295 | # print("helloFromRenderPassDecorator:", str(cls)) # For some reason, prints cant take place before the addon has been registered 296 | # If one uncomments the print, it wont ever show up in Blenders terminal 297 | 298 | 299 | cls.settings.bl_label = insertSpaceAfterCapital(cls.__name__) 300 | cls.settings.bl_idname = PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__) 301 | cls.bl_label = insertSpaceAfterCapital(cls.__name__) 302 | cls.bl_idname = PascalCaseTo_snake_case(cls.__name__) 303 | classesToRegister.append(cls.settings) 304 | classesToRegister.append(cls) 305 | settingsPropertyGroupParents.append(cls) 306 | renderPasses.append(cls) 307 | 308 | # cls = registerClassGivebl_labelAndbl_idnameWithUnderscore(cls) 309 | # cls = settingsContainerDecorator(cls) 310 | # renderPasses.append(cls) 311 | print(f"Awesomesauce: cls.__name__") 312 | return cls 313 | 314 | def registerClassToBPYDecorator(cls): 315 | # class NewClass(cls): 316 | # classesToRegister.append(cls) 317 | # return NewClass 318 | classesToRegister.append(cls) 319 | return cls 320 | 321 | def registerOperatorsDecorator(cls): 322 | # class NewClass(cls): 323 | # classesToRegister.append(cls) 324 | # cls.bl_idname = PascalCaseTo_snake_case(addonNameShort + "." + cls.__name__) 325 | # cls.bl_label = insertSpaceAfterCapital(cls.__name__) 326 | # # cls.bl_label = "Fast PBR viewport render" # Display name in the interface. 327 | # # cls.bl_options = {'DEFAULT_CLOSED'} # Enable undo for the operator. 328 | # # cls.bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator. 329 | # return NewClass 330 | 331 | 332 | # cls.bl_label = "Fast PBR viewport render" # Display name in the interface. 333 | # cls.bl_options = {'DEFAULT_CLOSED'} # Enable undo for the operator. 334 | # cls.bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator. 335 | return registerClassGivebl_labelAndbl_idnameWithDot(cls) 336 | 337 | 338 | ##################### 339 | # END OF DECORATORS # 340 | ##################### 341 | 342 | ##################### 343 | # This example adds an object mode tool to the toolbar. 344 | # This is just the circle-select and lasso tools tool. 345 | # import bpy 346 | 347 | # # from bpy.utils.toolsystem import ToolDef 348 | # from bpy.types import WorkSpaceTool 349 | 350 | # class MyTool(WorkSpaceTool): 351 | # bl_space_type='VIEW_3D' 352 | # bl_context_mode='OBJECT' 353 | 354 | # # The prefix of the idname should be your add-on name. 355 | # bl_idname = "my_template.my_circle_select" 356 | # bl_label = "My Circle Select" 357 | # bl_description = ( 358 | # "This is a tooltip\n" 359 | # "with multiple lines" 360 | # ) 361 | # bl_icon = "ops.generic.select_circle" 362 | # bl_widget = None 363 | # bl_keymap = ( 364 | # ("view3d.select_circle", {"type": 'LEFTMOUSE', "value": 'PRESS'}, 365 | # {"properties": [("wait_for_input", False)]}), 366 | # ("view3d.select_circle", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, 367 | # {"properties": [("mode", 'SUB'), ("wait_for_input", False)]}), 368 | # ) 369 | 370 | # def draw_settings(context, layout, tool): 371 | # props = tool.operator_properties("view3d.select_circle") 372 | # layout.prop(props, "mode") 373 | # layout.prop(props, "radius") 374 | 375 | 376 | # class MyOtherTool(WorkSpaceTool): 377 | # bl_space_type='VIEW_3D' 378 | # bl_context_mode='OBJECT' 379 | 380 | # bl_idname = "my_template.my_other_select" 381 | # bl_label = "My Lasso Tool Select" 382 | # bl_description = ( 383 | # "This is a tooltip\n" 384 | # "with multiple lines" 385 | # ) 386 | # bl_icon = "ops.generic.select_lasso" 387 | # bl_widget = None 388 | # bl_keymap = ( 389 | # ("view3d.select_lasso", {"type": 'LEFTMOUSE', "value": 'PRESS'}, None), 390 | # ("view3d.select_lasso", {"type": 'LEFTMOUSE', "value": 'PRESS', "ctrl": True}, 391 | # {"properties": [("mode", 'SUB')]}), 392 | # ) 393 | 394 | # def draw_settings(context, layout, tool): 395 | # props = tool.operator_properties("view3d.select_lasso") 396 | # layout.prop(props, "mode") 397 | 398 | 399 | # def register(): 400 | 401 | 402 | # def unregister(): 403 | 404 | 405 | # if __name__ == "__main__": 406 | # register() 407 | ################### 408 | 409 | # os.remove(file) 410 | #import os 411 | #import time 412 | 413 | ################ 414 | # UI OPERATORS # 415 | ################ 416 | 417 | # class SimpleOperator(bpy.types.Operator): 418 | # """Tooltip""" 419 | # bl_idname = "object.test" 420 | # bl_label = "Confirm" 421 | # confirm = bpy.props.EnumProperty( 422 | # name="Confirm", 423 | # items= [ 424 | # ('yes',"Yes",''), 425 | # ('no',"No",''), 426 | # ], 427 | # ) 428 | 429 | # def execute(self, context): 430 | # print(self.confirm) 431 | 432 | # return {'FINISHED'} 433 | 434 | # def invoke(self, context, event): 435 | # return context.window_manager.invoke_props_dialog(self) 436 | # def draw(self,context): 437 | # self.layout.prop(self, "confirm", expand=True) 438 | 439 | # bpy.utils.register_class(SimpleOperator) 440 | 441 | 442 | 443 | # bl_info = { 444 | # "name": "Multiple panel example", 445 | # "author": "Robert Guetzkow", 446 | # "version": (1, 0), 447 | # "blender": (2, 80, 0), 448 | # "location": "View3D > Sidebar > Example tab", 449 | # "description": "Example with multiple panels", 450 | # "warning": "", 451 | # "wiki_url": "", 452 | # "category": "3D View"} 453 | 454 | 455 | 456 | # classesToRegister.append(classesToRegister.append(EXAMPLE_PT_panel_1)) 457 | 458 | ####################################################################################### 459 | # @bookmark retrieve settings 460 | 461 | def retrieveSettings(cls): 462 | if TYPE_CHECKING: # TYPE_CHEKCING is only true when running through a language server. 463 | # Always False when the code is running in a real software. 464 | print("TYPE_CHEKCING IS RETURNING TRUE! IF YOURE ATTEMPTING TO RUN THE SOFTWARE, IT LIKELY WONT RUN AS LONG AS THIS IS THE CASE. Im using TYPE_CHEKCING to get some intellisense fixes, but it will as a result break the addon at runtime if TYPE_CHEKCING is true when its not supposed to.") 465 | return cls.settings 466 | else: # bpy.context.scene.fast_pbr_render_normal_pass_with_workbench 467 | # print("bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__)) 468 | # returnValue: Any 469 | # exec(("returnValue = bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__))) 470 | lcls = locals() 471 | # exec( "returnValue = bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__), globals(), lcls ) 472 | exec( f"""returnValue = bpy.context.scene.{PascalCaseTo_snake_case(addonNameShort + '_' + cls.bl_idname)}""", globals(), lcls ) 473 | returnValue = lcls["returnValue"] 474 | # print(f"returnValue: {returnValue}") 475 | # print(f"""returnValue code as string: returnValue = bpy.context.scene.{PascalCaseTo_snake_case(addonNameShort + '_' + cls.bl_idname)}""") 476 | return returnValue 477 | 478 | # Supposedly unused, but might be needed at some point in the future 479 | def retrieveSettingsUsing__name__insteadOfbl_idname(cls): 480 | if TYPE_CHECKING: # TYPE_CHEKCING is only true when running through a language server. 481 | # Always False when the code is running in a real software. 482 | return cls.settings 483 | else: # bpy.context.scene.fast_pbr_render_normal_pass_with_workbench 484 | # print("bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__)) 485 | # returnValue: Any 486 | # exec(("returnValue = bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__))) 487 | lcls = locals() 488 | # exec( "returnValue = bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__), globals(), lcls ) 489 | exec( f"""returnValue = bpy.context.scene.{PascalCaseTo_snake_case(addonNameShort + '_' + cls.__name__)}""", globals(), lcls ) 490 | returnValue = lcls["returnValue"] 491 | print(f"returnValue: {returnValue}") 492 | return returnValue 493 | 494 | ####################################################################################### 495 | 496 | 497 | ######################################## 498 | ###### PANEL AND UI REGISTRATION ####### 499 | ######################################## 500 | # @bookmark panel and ui reg 501 | 502 | class GRABDOC_OT_map_preview_warning(bpy.types.Operator): 503 | """Preview the selected material""" 504 | bl_idname = "grab_doc.preview_warning" 505 | bl_label = " MATERIAL PREVIEW WARNING" 506 | bl_options = {'INTERNAL'} 507 | 508 | preview_type: bpy.types.EnumProperty( 509 | items=( 510 | ('normals', "", ""), 511 | ('curvature', "", ""), 512 | ('occlusion', "", ""), 513 | ('height', "", ""), 514 | ('alpha', "", ""), 515 | ('albedo',"",""), 516 | ('ID', "", "") 517 | ), 518 | options={'HIDDEN'} 519 | ) 520 | 521 | def invoke(self, context, event): 522 | return context.window_manager.invoke_props_dialog(self, width = 550) 523 | 524 | def draw(self, context): 525 | layout = self.layout 526 | 527 | col = layout.column() 528 | col.separator() 529 | 530 | col.label(text = "Live Material Preview is a feature that allows you to view what your bake maps will look like") 531 | col.label(text = "in real-time. Consider this warning: This feature is strictly meant for viewing your materials &") 532 | col.label(text = "not for editing while inside a preview. Once finished previewing, please exit to avoid instability.") 533 | col.label(text = "") 534 | col.label(text = "Pressing 'OK' will dismiss this warning permanently for this project file.") 535 | 536 | def execute(self, context): 537 | context.scene.grabDoc.firstBakePreview = False 538 | 539 | bpy.ops.grab_doc.preview_map(preview_type = self.preview_type) 540 | return{'FINISHED'} 541 | 542 | # @registerOperatorsDecorator 543 | # class FastPBRViewportRender(bpy.types.Panel): 544 | # # self.__classess__. 545 | 546 | # bl_label = "Fast PBR Viewport Render" 547 | # bl_idname = "Fast_PBR_Viewport_Render" 548 | # bl_category = "Fast" 549 | # bl_space_type = "VIEW_3D" 550 | # bl_region_type = "UI" 551 | # bl_options = set() 552 | 553 | # def draw(self, context): 554 | # layout = self.layout 555 | # layout.operator("fast_pbr.fast_pbr_viewport_render") 556 | # layout.label(text="This is panel 1.") 557 | 558 | # layout.row().prop(bpy.context.scene.fast_pbr_render_normal_pass_with_workbench, "file_path") 559 | # row = self.layout.row().prop(retrieveSettings(RenderNormalPassWithWorkbench), "file_path") 560 | # row.separator(factor = .5) 561 | # row.prop(retrieveSettings(RenderNormalPassWithWorkbench), "second_file_path") 562 | 563 | # @bookmark panel base class 564 | class FastPanelBaseClass: 565 | bl_space_type = "VIEW_3D" 566 | # bl_region_type = "TOOLS" # This actually puts the panel 567 | # on the left side of the 3D viewport! Since the left panel is usually very 568 | # narrow however, I dont recommend putting it here. 569 | bl_region_type = "UI" 570 | bl_category = "Fast" 571 | class FastToolBarPanelBaseClass: 572 | bl_space_type = "VIEW_3D" 573 | # bl_region_type = "TOOLS" # This actually puts the panel 574 | # on the left side of the 3D viewport! Since the left panel is usually very 575 | # narrow however, I dont recommend putting it here. 576 | 577 | 578 | bl_region_type = "HEADER" 579 | bl_category = "Fast" 580 | # bl_options = {"DEFAULT_CLOSED"} 581 | import textwrap 582 | 583 | 584 | # parent could be self.layout 585 | # context is the second parameter in draw() 586 | # text is the text you want to wrap 587 | def label_multiline(text: str, context: bpy.types.Context, parent): 588 | textListSplitByNewLines = text.split('\n') 589 | 590 | for text in textListSplitByNewLines: 591 | chars = int(context.region.width / 7) # 7 pix on 1 character 592 | wrapper = textwrap.TextWrapper(width=chars) 593 | text_lines = wrapper.wrap(text=text) 594 | for text_line in text_lines: 595 | parent.label(text=text_line) 596 | 597 | def retrieveOperatorFromCls(cls): 598 | 599 | # if hasattr(cls, 'bl_idname'): 600 | # return cls.bl_idname 601 | # return PascalCaseTo_snake_case(f"{addonNameShort}_{cls.__name__}") 602 | return cls.bl_idname 603 | 604 | # @bookmark main panel 605 | 606 | @registerOperatorsDecorator 607 | class FastPBRViewportRender(FastPanelBaseClass, bpy.types.Panel): 608 | # bl_idname = "EXAMPLE_PT_panel_1" 609 | # bl_label = "Panel 1" 610 | 611 | def draw(self, context): 612 | # print("YOOOOOOOOOOOOOOOOOOOOOOOOOOOOO: " + retrieveOperatorFromCls(FastPBRViewportRender)) 613 | self.layout.operator(retrieveOperatorFromCls(FastPBRViewportRender)) 614 | self.layout.operator(retrieveOperatorFromCls(FastPBRRestoreSettings)) 615 | self.layout.operator(retrieveOperatorFromCls(MatchViewportDisplayAndSurfaceBaseColor)) 616 | # self.layout.row().prop(bpy.context.scene.fast_pbr_render_normal_pass_with_workbench, "file_path") 617 | # box = self.layout.box() 618 | # # box.separator_spacer = 0.1 619 | # box.label(text=f"Welcome to {addonNameShort}!") 620 | 621 | # box.row().prop(retrieveSettingsUsing__name__insteadOfbl_idname(RenderNormalPassWithWorkbench), "file_path") 622 | 623 | # self.layout.box() 624 | 625 | 626 | 627 | 628 | @registerOperatorsDecorator 629 | class WelcomeToFastPBRViewportRender(FastPanelBaseClass, bpy.types.Panel): 630 | bl_parent_id = FastPBRViewportRender.bl_idname 631 | 632 | def draw(self, context): 633 | # layout = self.layout 634 | # layout.label(text=f"{self.bl_label}First Sub Panel of Panel 1.") 635 | label_multiline(f"""This tool lets you automatically generate full sets of PBR images and export them using a naming standard of your choice. 636 | 637 | The addon supports a wide range of PBR types & is easily configurable, in addition to this it works by rendering your viewport, therefore 638 | rendering from a camera of your choice is as easy as looking through the camera and performing a render. 639 | 640 | If you have any questions or have any ideas, you can reach me via Discord, danieljackson#0286 or by visiting our Github.""",context,self.layout) 641 | 642 | class propertyClass(): 643 | def __str__(self): 644 | return str(f'{self=}'.split('=')[0]) 645 | testVar = propertyClass() 646 | 647 | 648 | # @registerOperatorsDecorator 649 | # class Dynamic2StringListElement(bpy.types.UIList): 650 | # # bl_idname = "BAKELAB_MAP_UL_list" 651 | # def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): 652 | # if self.layout_type in {'DEFAULT', 'COMPACT'}: 653 | # if item.type == 'CustomPass': 654 | # layout.label(text = item.pass_name, icon = 'NONE') 655 | # else: 656 | # layout.label(text = item.type, icon = 'NONE') 657 | # elif self.layout_type in {'GRID'}: 658 | # layout.alignment = 'CENTER' 659 | # layout.label(text = "", icon = 'TEXTURE') 660 | # layout.prop(item, 'enabled') 661 | 662 | 663 | 664 | ##################### 665 | ### DYAMIC UI LIST### 666 | ##################### 667 | # @bookmark dynamic ui list 668 | import bpy 669 | # from bpy.props import StringProperty, IntProperty, CollectionProperty 670 | # from bpy.types import PropertyGroup, UIList, Operator, Panel 671 | # bl_info = { 672 | # "name": "Sinestesia UI List Example", 673 | # "blender": (3, 00, 0), 674 | # "category": "Object", 675 | # } 676 | 677 | @settingsContainerDecorator 678 | class ListStringItem(): 679 | 680 | class settings(bpy.types.PropertyGroup): # The data container for the values stored on each item in the list. 681 | """Group of properties representing an item in the list.""" 682 | 683 | key: bpy.props.StringProperty( 684 | name="key", 685 | description="""This is the key that you put into the brackets to have it be replaced by the held value when generating the final path. Note that you are not supposed to enter the curly braces yourself here, you only put the braces when using the key in a path. 686 | 687 | An example value could be 'textureSetName'""", 688 | default="textureSetName") = propertyClass() 689 | 690 | value: bpy.props.StringProperty( 691 | name="value", 692 | description="This is the value that the key gets replaced by. Example 'myFancyTextureSet'", 693 | default="MyFancyTextureSet") = propertyClass() 694 | 695 | 696 | @registerOperatorsDecorator # Seems to work just fine with UILIST as well kek. 697 | class DynamicListItemUI(bpy.types.UIList): # Draws all the items in the list, its the box that the list items are displayed within. 698 | """Demo UIList.""" 699 | # bl_ 700 | 701 | def draw_item(self, context, layout: bpy.types.Panel.layout, data, item: ListStringItem.settings, icon, active_data, 702 | active_propname, index): # Draws the item itself 703 | # item = ListStringItem() 704 | # We could write some code to decide which icon to use here... 705 | # custom_icon = 'OBJECT_DATAMODE' 706 | custom_icon = 'NONE' 707 | 708 | # Make sure your code supports all 3 layout types 709 | if self.layout_type in {'DEFAULT', 'COMPACT'}: 710 | # item: ListStringItem 711 | 712 | # row = layout.row() 713 | layout.label(text=item.key, icon = custom_icon) # @todo Check if its possible to make these modifiable with double click, like as if they were displayed as a prop directly. 714 | layout.label(text=item.value, icon = custom_icon) 715 | # layout.prop(retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property, item.key) 716 | # layout.label(text=item.value, icon = custom_icon) 717 | 718 | 719 | # cls.layout.prop(retrieveSettings(cls), f'{cls.settings.enable_pass=}'.split('=')[0].split('.')[-1], text = "") 720 | 721 | # layout.label(text=item.name, icon = custom_icon) 722 | # layout.label(text=item.random_prop, icon = custom_icon) 723 | 724 | elif self.layout_type in {'GRID'}: # Not sure when this is supposed to trigger? 725 | layout.alignment = 'CENTER' # When is the ui displayed in a grid lol? 726 | layout.label(text="", icon = custom_icon) 727 | 728 | @registerOperatorsDecorator 729 | class DynamicListNewItemOperator(bpy.types.Operator): 730 | """Add a new item to the list.""" 731 | 732 | # bl_idname = "my_list.new_item" 733 | # bl_label = "Add a new item" 734 | 735 | def execute(self, context): 736 | retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property.add() 737 | 738 | return{'FINISHED'} 739 | 740 | 741 | @registerOperatorsDecorator 742 | class DynamicListDeleteOperator(bpy.types.Operator): 743 | """Delete the selected item from the list.""" 744 | 745 | # bl_idname = "my_list.delete_item" 746 | # bl_label = "Deletes an item" 747 | 748 | @classmethod 749 | def poll(cls, context): 750 | return retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property 751 | 752 | def execute(self, context): 753 | # my_list = context.scene.my_list 754 | # index = context.scene.list_index 755 | my_list = retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property 756 | index = retrieveSettings(GlobalSettings).file_path_replacement_keys_index_selected_by_user 757 | 758 | my_list.remove(index) 759 | index = min(max(0, index - 1), len(my_list) - 1) 760 | 761 | return{'FINISHED'} 762 | 763 | @registerOperatorsDecorator 764 | class DynamicListMoveOperator(bpy.types.Operator): 765 | """Move an item in the list.""" 766 | 767 | # bl_idname = "my_list.move_item" 768 | # bl_label = "Move an item in the list" 769 | 770 | direction: bpy.props.EnumProperty(items=(('UP', 'Up', ""), 771 | ('DOWN', 'Down', ""),)) 772 | 773 | @classmethod 774 | def poll(cls, context): 775 | return retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property 776 | 777 | def move_index(self): 778 | """ Move index of an item render queue while clamping it. """ 779 | 780 | index = retrieveSettings(GlobalSettings).file_path_replacement_keys_index_selected_by_user 781 | list_length = len(retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property) - 1 # (index starts at 0) 782 | new_index = index + (-1 if self.direction == 'UP' else 1) 783 | 784 | index = max(0, min(new_index, list_length)) 785 | 786 | def execute(self, context): 787 | my_list = retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property 788 | index = retrieveSettings(GlobalSettings).file_path_replacement_keys_index_selected_by_user 789 | 790 | neighbor = index + (-1 if self.direction == 'UP' else 1) 791 | my_list.move(neighbor, index) 792 | self.move_index() 793 | 794 | return{'FINISHED'} 795 | 796 | # @bookmark UI list panel 797 | 798 | # class GlobalSettings(): 799 | # pass 800 | 801 | 802 | 803 | 804 | # def register(): 805 | 806 | # bpy.utils.register_class(ListItem) 807 | # bpy.utils.register_class(MY_UL_List) 808 | # bpy.utils.register_class(LIST_OT_NewItem) 809 | # bpy.utils.register_class(LIST_OT_DeleteItem) 810 | # bpy.utils.register_class(LIST_OT_MoveItem) 811 | # bpy.utils.register_class(PT_ListExample) 812 | 813 | # bpy.types.Scene.my_list = CollectionProperty(type = ListItem) 814 | # bpy.types.Scene.list_index = IntProperty(name = "Index for my_list", 815 | # default = 0) 816 | 817 | 818 | # def unregister(): 819 | 820 | # del bpy.types.Scene.my_list 821 | # del bpy.types.Scene.list_index 822 | 823 | # bpy.utils.unregister_class(ListItem) 824 | # bpy.utils.unregister_class(MY_UL_List) 825 | # bpy.utils.unregister_class(LIST_OT_NewItem) 826 | # bpy.utils.unregister_class(LIST_OT_DeleteItem) 827 | # bpy.utils.unregister_class(LIST_OT_MoveItem) 828 | # bpy.utils.unregister_class(PT_ListExample) 829 | 830 | 831 | # if __name__ == "__main__": 832 | # register() 833 | 834 | ######### UI LIST EXAMPLE END 835 | 836 | 837 | @registerOperatorsDecorator 838 | class RenderPasses(FastPanelBaseClass, bpy.types.Panel): 839 | bl_parent_id = FastPBRViewportRender.bl_idname 840 | 841 | def draw(self, context): 842 | pass 843 | 844 | 845 | # @bookmark GlobalSettings 846 | 847 | # def getFutureClass(cls): 848 | # if TYPE_CHECKING: 849 | # return cls.settings 850 | 851 | class texts(): 852 | MoveAndRenameOperatorDescription = f'''Move and rename lets you move all the images from your source folder to your destination folder, specified by the respective fields. The source path supports variable/key insertion using the {{myVariable}} format, however unlike the destination path it ONLY supports user defined variables and a smaller subset of predefined variables. Variables like imageSizeShort or imageName are therefore not supported. 853 | 854 | Supported predefined variables for the source path are: 855 | 856 | fullPathToDesktop 857 | fullPathToUserHomePath 858 | 859 | The intended usecase for this feature is that if youre exporting images from a 3rd party software such as Photoshop, Houdini, Quixel Mixer or Adobe Substance (or any other software that exports images :P ) you can still use {addonName} to name the images by your personal or your projects naming standards with help of the addons file name/path generation. 860 | 861 | For instance if you export some 4K images from Mixer to have something like the following paths: 862 | 863 | "C:/FromMixer/albedo.png" 864 | "C:/FromMixer/roughness.png" 865 | "C:/FromMixer/metalness.png" 866 | "C:/FromMixer/normal.png" 867 | "C:/FromMixer/displacement.png" 868 | "C:/FromMixer/ao.png" 869 | 870 | You can use the following source: 871 | 872 | "C:/FromMixer" 873 | 874 | and target path: 875 | 876 | "{{fullPathToDesktop}}/Art And Development/{{name}}/{{sourceDirectoryTopLevelFolderName}}{{imageSizeShort}}/{{name}}{{imageSizeShort}}_{{fileNameWithExtension}}" 877 | 878 | You will have your images be moved and renamed to something like: 879 | 880 | "C:/Users/Oliver/desktop/Art And Development/MyFancyTextureSet/FromMixer4K/MyFancyTextureSet4K_albedo.png" 881 | "C:/Users/Oliver/desktop/Art And Development/MyFancyTextureSet/FromMixer4K/MyFancyTextureSet4K_roughness.png" 882 | "C:/Users/Oliver/desktop/Art And Development/MyFancyTextureSet/FromMixer4K/MyFancyTextureSet4K_metalness.png" 883 | "C:/Users/Oliver/desktop/Art And Development/MyFancyTextureSet/FromMixer4K/MyFancyTextureSet4K_normal.png" 884 | "C:/Users/Oliver/desktop/Art And Development/MyFancyTextureSet/FromMixer4K/MyFancyTextureSet4K_displacement.png" 885 | "C:/Users/Oliver/desktop/Art And Development/MyFancyTextureSet/FromMixer4K/MyFancyTextureSet4K_ao.png" 886 | 887 | Magical right? 888 | 889 | Note that the souce path should be pointing towards a folder, not a particular image! Moving individual images is unsupported currently, it always moves all images that it can find directly under the specified directory. 890 | ''' 891 | exportPathDescription = f"""The export path lets you choose the name 892 | of the images as well as the full path those images goes inside upon export using 893 | pre-defined variables that are auto-generated as well as variables that you define yourself. 894 | 895 | There is a large set of predefined variables: 896 | 897 | imageSizeShort 898 | imageWidth 899 | imageHeight 900 | imageSizeInPixels 901 | fileNameWithExtension 902 | fileNameWithoutExtension 903 | fileExtensionWithoutDot 904 | fileWithPathAndExtension 905 | sourceDirectoryTopLevelFolderName 906 | sourceDirectory 907 | 908 | fullPathToDesktop 909 | fullPathToUserHomePath 910 | 911 | In addition to these, you can define your own using the dynamic list below, but first, heres an example of what a file path could look like: 912 | C:/testTarget/{{name}}/FromFastPBR/{{name}}{{imageSizeShort}}_{{fileNameWithExtension}} 913 | Which would then turn into something like: 914 | C:/testTarget/myFancyTextureSet/FromFastPBR/myFancyTextureSet4K_AO.png 915 | """ 916 | 917 | 918 | 919 | 920 | @settingsContainerDecorator 921 | @registerOperatorsDecorator 922 | class GlobalSettings(FastPanelBaseClass, bpy.types.Panel): 923 | bl_parent_id = FastPBRViewportRender.bl_idname 924 | # bl_options = {'HIDE_HEADER'} 925 | # bl_options = {} 926 | bl_order = 1000000 # Doesnt seem to work ;-; 927 | class settings(bpy.types.PropertyGroup): # CANNOT BE RENAMED 928 | # def __init_subclass__(cls, scm_type=None, name=None, **kwargs): 929 | # print(f"initializing subclass: {cls}") 930 | # classesToRegister.append(cls) 931 | export_source_path: bpy.props.StringProperty(name="Source Path", 932 | description=texts.MoveAndRenameOperatorDescription, 933 | default="", 934 | maxlen=1024, 935 | subtype="FILE_PATH") 936 | export_file_path: bpy.props.StringProperty(name="Destination path", 937 | description=texts.exportPathDescription, 938 | default="", 939 | maxlen=1024, 940 | subtype="FILE_PATH") 941 | second_file_path: bpy.props.StringProperty(name="File path", # Unused I think? Should be safe to remove if you feel brave 942 | description="Some elaborate description", 943 | default="", 944 | maxlen=1024, 945 | subtype="FILE_PATH") 946 | enable_pass: bpy.props.BoolProperty(name="Enable Pass", default=True) = propertyClass() # Also unused? 947 | file_path_replacement_keys_collection_property: bpy.props.CollectionProperty(type=ListStringItem.settings) = propertyClass() 948 | file_path_replacement_keys_index_selected_by_user: bpy.props.IntProperty(name = "Index Selected by the User.", default = 0) = propertyClass() 949 | # def register(): 950 | # bpy.types.Scene.my_list = CollectionProperty(type = ListItem) 951 | # bpy.types.Scene.list_index = IntProperty(name = "Index for my_list", 952 | # default = 0) 953 | 954 | def draw(self, context): 955 | # layout = self.layout 956 | descriptiveText = f"""The export path lets you choose the name 957 | of the images as well as the full path those images goes inside upon export using 958 | pre-defined variables that are auto-generated as well as variables that you define yourself. 959 | 960 | There is a large set of predefined variables: 961 | 962 | imageSizeShort 963 | imageWidth 964 | imageHeight 965 | imageSizeInPixels 966 | fileNameWithExtension 967 | fileNameWithoutExtension 968 | fileExtension 969 | fileWithPathAndExtension 970 | 971 | In addition to these, you can define your own using the dynamic list below, but first, heres an example of what a file path could look like: 972 | C:/testTarget/{{name}}/FromFastPBR/{{name}}{{imageSizeShort}}_{{fileNameWithExtension}} 973 | Which would then turn into something like: 974 | C:/testTarget/myFancyTextureSet/FromFastPBR/myFancyTextureSet4K_AO.png 975 | """ 976 | 977 | 978 | box = self.layout.box() 979 | # box.row().prop(retrieveSettingsUsing__name__insteadOfbl_idname(RenderNormalPassWithWorkbench), "file_path") 980 | box.row().prop(retrieveSettings(self), "export_file_path") 981 | # label_multiline(descriptiveText, context, box) 982 | # box.row().template_list(Dynamic2StringListElement.bl_idname, "", context.scene) 983 | 984 | 985 | @registerOperatorsDecorator 986 | class CustomPathKeys(FastPanelBaseClass,bpy.types.Panel): 987 | """Demo panel for UI list Tutorial.""" 988 | 989 | # bl_label = "UI_List Demo" 990 | # bl_idname = "SCENE_PT_LIST_DEMO" 991 | # bl_space_type = 'PROPERTIES' 992 | # bl_region_type = 'WINDOW' 993 | # bl_context = "scene" 994 | # bl_options = {'HIDE_HEADER'} 995 | # bl_options = {'DRAW_BOX', 'HIDE_HEADER'} 996 | # bl_parent_id = PascalCaseTo_snake_case(f"{addonNameShort}GlobalSettings") 997 | # bl_parent_id = PascalCaseTo_snake_case(f"{addonNameShort}ViewportRender") 998 | bl_parent_id = GlobalSettings.bl_idname 999 | 1000 | # def draw_header(cls, context): # The draw header is the first line of the panel where the foldout arrow is. 1001 | # # layout = self.layout 1002 | # # cls.layout.prop(retrieveSettings(cls), f'{cls.settings.enable_pass=}'.split('=')[0].split('.')[-1], text = "") 1003 | # cls.layout.row().label("hi") 1004 | 1005 | def draw(self, context): 1006 | layout = self.layout 1007 | scene = context.scene 1008 | 1009 | outerVerticalBox = layout.row().box() 1010 | # print("LOOK MAN: " + str(retrieveSettings(GlobalSettings))) 1011 | # # print("LOOK MAN: " + f'{GlobalSettings.settings.file_path_replacement_keys_collection_property=}'.split('=')[0].split('.')[-1]) 1012 | # print("LOOK MAN: " + f'{GlobalSettings.settings.file_path_replacement_keys_index_selected_by_user=}'.split('=')[0].split('.')[-1]) 1013 | outerVerticalBox.template_list(DynamicListItemUI.bl_idname, "wtf_is_this_for", retrieveSettings(GlobalSettings), 1014 | str(f'{GlobalSettings.settings.file_path_replacement_keys_collection_property=}'.split('=')[0].split('.')[-1]), retrieveSettings(GlobalSettings), str(f'{GlobalSettings.settings.file_path_replacement_keys_index_selected_by_user=}'.split('=')[0].split('.')[-1])) 1015 | 1016 | 1017 | # cls.layout.prop(retrieveSettings(cls), f'{cls.settings.enable_pass=}'.split('=')[0].split('.')[-1]) 1018 | 1019 | # row.template_list(MY_UL_List.bl_idname, "wtf_is_this_for", bpy.context.scene.fast_pbr_global_settings, 1020 | # 'file_path_replacement_keys_collection_property', bpy.context.scene.fast_pbr_global_settings, "file_path_replacement_keys_i 1021 | 1022 | # row.template_list(MY_UL_List.bl_idname, "wtf_is_this_for", bpy.context.scene.fast_pbr_global_settings, 1023 | # 'file_path_replacement_keys_collection_property', bpy.context.scene.fast_pbr_global_settings, "file_path_replacement_keys_index_selected_by_user") 1024 | # BACKUP: 1025 | # row.template_list(MY_UL_List.bl_idname, "wtf_is_this_for", retrieveSettings(GlobalSettings), 1026 | # f'{GlobalSettings.settings.file_path_replacement_keys_collection_property=}'.split('=')[0], retrieveSettings(GlobalSettings), f'{GlobalSettings.settings.file_path_replacement_keys_index_selected_by_user=}'.split('=')[0]) 1027 | 1028 | 1029 | 1030 | # self.layout.operator(retrieveOperatorFromCls(FastPBRViewportRender)) 1031 | 1032 | # MEANT TO BE ACTIVE: 1033 | # innerRow = row.box() 1034 | innerHorizontalRow = outerVerticalBox.row() 1035 | # row.operator(PascalCaseTo_snake_case(f"{addonNameShort}.{DynamicListNewItemOperator}"), text='NEW') 1036 | innerHorizontalRow.operator(retrieveOperatorFromCls(DynamicListNewItemOperator), text='NEW') 1037 | innerHorizontalRow.operator(retrieveOperatorFromCls(DynamicListDeleteOperator), text='REMOVE') 1038 | innerHorizontalRow.operator(retrieveOperatorFromCls(DynamicListMoveOperator), text='UP').direction = 'UP' 1039 | innerHorizontalRow.operator(retrieveOperatorFromCls(DynamicListMoveOperator), text='DOWN').direction = 'DOWN' 1040 | 1041 | if retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property: 1042 | item = retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property[retrieveSettings(GlobalSettings).file_path_replacement_keys_index_selected_by_user] 1043 | 1044 | # outerVerticalBox = outerVerticalBox.row() 1045 | # outerVerticalBox.prop(item, retrieveSettings(ListStringItem).key) 1046 | outerVerticalBox.prop(item, "key") 1047 | outerVerticalBox.prop(item, "value") 1048 | 1049 | # if retrieveSettings(GlobalSettings).file_path_replacement_keys_index_selected_by_user >= 0 and retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property: 1050 | # item = retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property[retrieveSettings(GlobalSettings).file_path_replacement_keys_index_selected_by_user] 1051 | 1052 | # row = layout.row() 1053 | # row.prop(item, "name") 1054 | # row.prop(item, "random_prop") 1055 | 1056 | 1057 | 1058 | 1059 | 1060 | # layout = self.layout 1061 | # box = self.layout.box() 1062 | # box.row().prop(retrieveSettingsUsing__name__insteadOfbl_idname(RenderNormalPassWithWorkbench), "file_path") 1063 | # label_multiline(f"""The export path lets you choose the name 1064 | # of the images as well as the full path those images goes inside upon export using 1065 | # pre-defined variables that are auto-generated as well as variables that you define yourself. 1066 | 1067 | # Heres an example of what a file path could look like: 1068 | 1069 | # """) 1070 | # layout.label(text="Second Sub Panel of Panel 1.") 1071 | 1072 | 1073 | 1074 | @registerOperatorsDecorator 1075 | class MoveAndRenameImages(bpy.types.Operator): 1076 | bl_description = texts.MoveAndRenameOperatorDescription 1077 | def execute(self, context: bpy.types.Context): 1078 | 1079 | replacementDictionary = dict() 1080 | 1081 | for item in retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property: 1082 | # print(str(item.value) + "item") 1083 | replacementDictionary[item.key] = item.value 1084 | 1085 | print(f""" 1086 | replacementDictionary: {replacementDictionary} 1087 | pathToStoreImagesIn: {pathToStoreImagesIn} 1088 | retrieveSettings(GlobalSettings).export_file_path: {retrieveSettings(GlobalSettings).export_file_path}""") 1089 | 1090 | 1091 | moveImages(retrieveSettings(GlobalSettings).export_source_path, retrieveSettings(GlobalSettings).export_file_path, replacementDictionary) 1092 | # moveImages(retrieveSettings(GlobalSettings).export_source_path, retrieveSettings(GlobalSettings).export_file_path, dict(retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property)) 1093 | 1094 | return {'FINISHED'} 1095 | 1096 | 1097 | def MoveAndRenameDecorator(cls: MoveAndRename): 1098 | cls.bl_parent_id = FastPBRViewportRender.bl_idname 1099 | 1100 | @MoveAndRenameDecorator 1101 | @registerOperatorsDecorator 1102 | class MoveAndRename(FastPanelBaseClass, bpy.types.Panel): 1103 | # bl_parent_id = GlobalSettings.bl_idname # Moved into decorator 1104 | def draw(self, context): 1105 | box = self.layout.box() 1106 | box.row().prop(retrieveSettings(GlobalSettings), "export_source_path") 1107 | self.layout.row().operator(retrieveOperatorFromCls(MoveAndRenameImages)) 1108 | # classesToRegister.append(classesToRegister.append(EXAMPLE_PT_panel_3)) 1109 | 1110 | # classesToRegister = (EXAMPLE_PT_panel_1, EXAMPLE_PT_panel_2, EXAMPLE_PT_panel_3) 1111 | 1112 | # row = self.layout.row(align = True) 1113 | # row.separator(factor = .5) 1114 | # row.prop(grabDoc, 'exportNormals', text = "") 1115 | 1116 | # row.operator("grab_doc.preview_warning" if grabDoc.firstBakePreview else "grab_doc.preview_map", text = "Normals Preview").preview_type = 'normals' 1117 | 1118 | # row.operator("grab_doc.offline_render", text = "", icon = "RENDER_STILL").render_type = 'normals' 1119 | # row.separator(factor = 1.3) 1120 | # layout.row().prop(retrieveSettings(RenderNormalPassWithWorkbench), "second_file_path") 1121 | 1122 | 1123 | 1124 | # layout.row().prop(RenderNormalPassWithWorkbench.retrieveSettings(), "file_path") 1125 | 1126 | 1127 | # layout.row().prop(context.scene.fast_pbr, "file_path") 1128 | # layout.row().prop(getGlobalAddonProperties(), "target_directory") 1129 | # layout.row().prop(context.preferences.addons[__name__].preferences, "file_path") 1130 | 1131 | 1132 | 1133 | # class EXAMPLE_panel: 1134 | # bl_space_type = "VIEW_3D" 1135 | # bl_region_type = "UI" 1136 | # bl_category = "Example Tab" 1137 | # bl_options = {"DEFAULT_CLOSED"} 1138 | 1139 | # @registerOperatorsDecorator 1140 | # class EXAMPLE_PT_panel_1(EXAMPLE_panel, bpy.types.Panel): 1141 | # bl_idname = "EXAMPLE_PT_panel_1" 1142 | # bl_label = "Panel 1" 1143 | 1144 | # def draw(self, context): 1145 | # layout = self.layout 1146 | # layout.label(text="This is the main panel.") 1147 | # @registerOperatorsDecorator 1148 | # class EXAMPLE_PT_panel_2(EXAMPLE_panel, bpy.types.Panel): 1149 | # bl_parent_id = "EXAMPLE_PT_panel_1" 1150 | # bl_label = "Panel 2" 1151 | 1152 | # def draw(self, context): 1153 | # layout = self.layout 1154 | # layout.label(text="First Sub Panel of Panel 1.") 1155 | # @registerOperatorsDecorator 1156 | # class EXAMPLE_PT_panel_3(EXAMPLE_panel, bpy.types.Panel): 1157 | # bl_parent_id = "EXAMPLE_PT_panel_1" 1158 | # bl_label = "Panel 3" 1159 | 1160 | 1161 | 1162 | 1163 | 1164 | 1165 | 1166 | # if __name__ == "__main__": 1167 | # register() 1168 | 1169 | 1170 | # PSUEDO CODE FOR A SETTINGS BACKUP/RESOTORE SYSTEM. 1171 | 1172 | # myDictionary = {"test": bpy.context.world} 1173 | 1174 | # @bookmark BACKUP RESTORE 1175 | class BackupPrepareAndRestore(): 1176 | 1177 | show_overlaysBackup: bpy.types.Space 1178 | activeWorldBackup: bpy.types.World 1179 | render_passBackup = str 1180 | shading_type: str = str() 1181 | 1182 | 1183 | @classmethod 1184 | def backupSettingsAndPrepareForRender(self): 1185 | # myDictionary[bpy.context.world] = "someRandomWorldThatINeedDuringTheExecutionOfTheScript - but that I later on wanna bring back to previous state" 1186 | 1187 | self.show_overlaysBackup = bpy.context.space_data.overlay.show_overlays 1188 | self.activeWorldBackup = bpy.context.scene.world 1189 | 1190 | self.render_passBackup = bpy.context.space_data.shading.render_pass 1191 | 1192 | self.shading_type = bpy.context.space_data.shading.type 1193 | 1194 | 1195 | 1196 | # bpy.context.world 1197 | global test 1198 | test = bpy.context 1199 | 1200 | 1201 | 1202 | bpy.ops.ed.undo_push() 1203 | 1204 | @classmethod 1205 | def restoreSettings(self): 1206 | disableRestore = False 1207 | if not disableRestore: 1208 | 1209 | bpy.ops.ed.undo() # This restores any settings 1210 | # in the file, however certain "settings" that 1211 | # are frequently toggled such as "overlays" (visibility 1212 | # of things like the 3D cursor & selected object outlines) 1213 | # Do not get affected by the undo action! 1214 | 1215 | if bpy.context.scene.render.engine == 'BLENDER_EEVEE': 1216 | bpy.context.space_data.shading.render_pass = self.render_passBackup 1217 | 1218 | bpy.context.space_data.shading.type = self.shading_type 1219 | 1220 | # pass 1221 | # for key in myDictionary: 1222 | # key = myDictionary[key] 1223 | # bpy.context = test 1224 | bpy.context.space_data.overlay.show_overlays = self.show_overlaysBackup 1225 | # bpy.context.scene.world = bpy.data.worlds.get(self.activeWorldBackup.name_full) 1226 | 1227 | 1228 | 1229 | 1230 | # def prepareScene(): 1231 | # bpy.context.space_data.overlay.show_overlays = False 1232 | 1233 | # space_shading = {sett: getattr(bpy.context.space_data.display.shading, sett) for sett in dir(bpy.context.space_data.display.shading)} 1234 | # # blabla 1235 | # for sett in space_shading: 1236 | # try: 1237 | # setattr(bpy.context.space_data.display.shading, sett, space_shading[sett]) 1238 | # except Exception as e: 1239 | # pass 1240 | 1241 | 1242 | 1243 | ################################################################################### 1244 | ######################### CLASSES FOR PASSES ###################################### 1245 | ################################################################################### 1246 | # bpy.types.collec 1247 | # @bookmark RenderPass base class 1248 | 1249 | 1250 | 1251 | # @renderPassDecorator 1252 | 1253 | # @renderPassDecorator 1254 | # class GlobalSettings(): 1255 | # class settings(bpy.types.PropertyGroup): 1256 | # export_file_path: bpy.props.StringProperty(name="Export path", 1257 | # description="The path that the images will be exported to, including the image name and file extension.", 1258 | # default="", 1259 | # maxlen=1024, 1260 | # subtype="FILE_PATH") 1261 | 1262 | class RenderPass(FastPanelBaseClass, bpy.types.Panel): 1263 | bl_options = {'DEFAULT_CLOSED'} 1264 | # bl_options = {'DRAW_BOX', 'DEFAULT_CLOSED'} 1265 | # @registerOperatorsDecorator 1266 | 1267 | class settings(bpy.types.PropertyGroup): # CANNOT BE RENAMED 1268 | # def __init_subclass__(cls, scm_type=None, name=None, **kwargs): 1269 | # print(f"initializing subclass: {cls}") 1270 | # classesToRegister.append(cls) 1271 | file_path: bpy.props.StringProperty(name="File path", 1272 | description="Some elaborate description", 1273 | default="", 1274 | maxlen=1024, 1275 | subtype="FILE_PATH") 1276 | second_file_path: bpy.props.StringProperty(name="File path", 1277 | description="Some elaborate description", 1278 | default="", 1279 | maxlen=1024, 1280 | subtype="FILE_PATH") 1281 | enable_pass: bpy.props.BoolProperty(name="Enable Pass", default=True) = propertyClass() 1282 | 1283 | # myVar123: str = "MY VAR!!!" 1284 | 1285 | global classesToRegister 1286 | classesToRegister.append(settings) 1287 | 1288 | ########## 1289 | ### UI ### 1290 | ########## 1291 | 1292 | bl_label = '' # Set in decorator by class name 1293 | bl_parent_id = RenderPasses.bl_idname # The parent ID is the panel name 1294 | # bl_parent_id = PascalCaseTo_snake_case(f"{addonNameShort}_global_settings") # The parent ID is the panel name 1295 | # that the coresponding panel will be in 1296 | def draw_header(cls, context): # The draw header is the first line of the panel where the foldout arrow is. 1297 | # layout = self.layout 1298 | cls.layout.prop(retrieveSettings(cls), f'{cls.settings.enable_pass=}'.split('=')[0].split('.')[-1], text = "") 1299 | # layout.label(text="My Select Panel") 1300 | 1301 | 1302 | def draw(cls, context): 1303 | # layout = self.layout 1304 | # cls.layout.label(text=f"{cls.bl_label}First Sub Panel of Panel 1.") 1305 | # print("LOOK HERE ", f'{cls.settings.enable_pass=}'.split('=')[0].split('.')[-1]) 1306 | cls.layout.prop(retrieveSettings(cls), f'{cls.settings.enable_pass=}'.split('=')[0].split('.')[-1]) 1307 | cls.draw_pass_specific_ui_elements(context) 1308 | 1309 | 1310 | def draw_pass_specific_ui_elements(cls, context): 1311 | pass 1312 | 1313 | ########## 1314 | # UI END # 1315 | ########## 1316 | 1317 | @classmethod 1318 | def retrieveSettings(cls): # CANNOT BE RENAMED 1319 | if TYPE_CHECKING: # TYPE_CHEKCING is only true when running through a language server. 1320 | # Always False when the code is running in a real software. 1321 | return cls.settings 1322 | else: # bpy.context.scene.fast_pbr_render_normal_pass_with_workbench 1323 | return print("bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__)) 1324 | return exec("bpy.context.scene." + PascalCaseTo_snake_case(addonNameShort + "_" + cls.__name__)) 1325 | # exec exectues a string as if it were written without the string 1326 | # Here were using it to access classes dynamically at runtime. 1327 | # Returns something like bpy.context.scene.fast_pbr.render_pass 1328 | 1329 | 1330 | instances: list[RenderPass] = [] 1331 | 1332 | passName = "UnnamedChannel" # Something simple, like "normal", "ao", "height", "matID" etc. Used for naming the files 1333 | # Should be overriden by each pass "subclass", or the passes will end up overriding the same file upon creation! 1334 | 1335 | # @classmethod 1336 | # def __init__(self): 1337 | # self.__class__.instances.append(self) 1338 | @classmethod 1339 | def prepare_to_render(self): 1340 | pass 1341 | 1342 | @classmethod 1343 | def render(self): 1344 | bpy.ops.render.opengl() 1345 | # bpy.ops.render.opengl(write_still = True, sequencer=False) 1346 | # print("---------------------------------------------------rendering") 1347 | 1348 | @classmethod 1349 | def save_to_disk(self): 1350 | # print("-----------------------------------------------------saving") 1351 | image = bpy.data.images.get("Render Result") 1352 | # image.filepath = pathToStoreImagesIn 1353 | # image.save_render(pathToStoreImagesIn, "test") 1354 | # image.filepath_raw = 'C:\FastPBRViewportRender' 1355 | # image.file_format = 'PNG' 1356 | # pathToStoreImageInIncludingFileNameAndFileExtension = 'C:/FastPBRViewportRender/' 1357 | pathToStoreImageInIncludingFileNameAndFileExtension = pathToStoreImagesIn 1358 | pathToStoreImageInIncludingFileNameAndFileExtension = pathToStoreImageInIncludingFileNameAndFileExtension + self.passName + ".png" 1359 | # bpy.context.scene.render.__format__ 1360 | # image = bpy.data.images.new("Sprite", alpha=True, width=16, height=16) 1361 | # image.alpha_mode = 'STRAIGHT' 1362 | # image.filepath_raw = "pathToStoreImageInIncludingFileNameAndFileExtension" 1363 | # image.file_format = 'PNG' 1364 | # image.save() 1365 | 1366 | # bpy.data.images['Render Result'].filepath_raw = pathToStoreImageInIncludingFileNameAndFileExtension 1367 | # bpy.data.images['Render Result'].file_format = 'PNG' 1368 | # bpy.data.images['Render Result'].save() 1369 | image.save_render(pathToStoreImageInIncludingFileNameAndFileExtension) 1370 | 1371 | 1372 | # image.save_render('C:\FastPBRViewportRender\mytexture.png') 1373 | # bpy.ops.image.save_as(save_as_render=True, copy=True, filepath="//..\\..\\untitled321.png", relative_path=True, show_multiview=False, use_multiview=False) 1374 | # bpy.ops.image. 1375 | 1376 | @classmethod 1377 | def perform_post_process(self = None): 1378 | """Runs after save_to_disk""" 1379 | pass 1380 | # @bookmark _Debugsession 66 1381 | @classmethod 1382 | def prepare_render_and_save(self): 1383 | self.prepare_to_render() 1384 | if retrieveSettingsUsing__name__insteadOfbl_idname(self).enable_pass: 1385 | print(f"§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§§ {self} -> {retrieveSettingsUsing__name__insteadOfbl_idname(self).enable_pass}") 1386 | self.render() 1387 | self.save_to_disk() 1388 | self.perform_post_process() 1389 | 1390 | # classesToRegister.append(classesToRegister.append(RenderPass)) 1391 | # @bookmark Normal pass 1392 | 1393 | 1394 | class RenderAppendedMaterialTypeRenderPass(RenderPass): # A base class for render passes that are supposed to render a specific material only 1395 | f"""To use this class, assign the literal name of the material in our assets file that you want to render to the materialToRender variable. 1396 | 1397 | Children of this class are typically found towards the bottom of the render pass section as its rendered in Eeevee.""" 1398 | 1399 | materialToRender = 'FastPBRNormal' 1400 | """This is the literal name of the material in our assets file that you want to render!""" 1401 | 1402 | @classmethod 1403 | def prepare_to_render(self): 1404 | bpy.context.space_data.shading.render_pass = 'DIFFUSE_COLOR' 1405 | bpy.context.scene.render.engine = 'BLENDER_EEVEE' 1406 | # bpy.context 1407 | # bpy.context.scene.render.engine = 'RENDER' 1408 | # Switch viewport view to rendered, rather than solid or wire or whatever. 1409 | bpy.context.space_data.shading.type = 'RENDERED' 1410 | 1411 | 1412 | 1413 | print("File path:", pathlib.Path(__file__).parent.resolve()) 1414 | 1415 | if bpy.data.materials.find(self.materialToRender) < 0: 1416 | appendAssetFromAssetsBlendFile(self.materialToRender, 'Material') 1417 | if bpy.data.objects.find(self.materialToRender) < 0: 1418 | appendAssetFromAssetsBlendFile(self.materialToRender, 'Object') 1419 | bpy.data.objects[self.materialToRender].visible_camera = False 1420 | bpy.data.objects[self.materialToRender].hide_render = True 1421 | bpy.data.objects[self.materialToRender].hide_viewport = True 1422 | # Skipping to loop over collections to save some precious execution time of looping over collections. The block is otherwise perfectly functional 1423 | # for collection in bpy.context.scene.collection: 1424 | # collection: bpy.types.Collection 1425 | # collection.objects.unlink(bpy.data.objects[self.materialToRender]) 1426 | # bpy.context.scene.collection.objects.unlink(bpy.data.objects[self.materialToRender]) 1427 | 1428 | 1429 | 1430 | 1431 | for object in bpy.data.objects: 1432 | object: bpy.types.Object 1433 | if hasattr(object.data, 'materials'): 1434 | if object.material_slots.__len__() < 1: 1435 | # bpy.ops.object.material_slot_add() 1436 | # object.material_slots[0].material[0] = bpy.data.materials.get(self.materialToRender) 1437 | object.data.materials.append(None) 1438 | # cube.material_slots[0].material = bpy.data.materials['Blue'] 1439 | for materialSlot in object.material_slots: 1440 | materialSlot: bpy.types.MaterialSlot 1441 | # materialSlot.link = 'FastPBRNormal' 1442 | materialSlot.material = bpy.data.materials.get(self.materialToRender) 1443 | copyModifiers(bpy.data.objects[self.materialToRender], bpy.data.objects) 1444 | # raise ValueError('A very specific bad thing happened.') 1445 | 1446 | @classmethod 1447 | def perform_post_process(self = None): 1448 | pathToStoreImageInIncludingFileNameAndFileExtension = pathToStoreImagesIn 1449 | fileToPerformPostProcessOn = pathToStoreImageInIncludingFileNameAndFileExtension + self.passName + ".png" 1450 | 1451 | # print("hello from post process") 1452 | # from PIL import Image 1453 | # import numpy 1454 | 1455 | im = Image.open(fileToPerformPostProcessOn) 1456 | im = im.convert('RGBA') 1457 | 1458 | data = numpy.array(im) # "data" is a height x width x 4 numpy array 1459 | red, green, blue, alpha = data.T # Temporarily unpack the bands for readability 1460 | 1461 | 1462 | white_areas = (red < 3) & (blue < 3) & (green < 3) # Condition for writing 1463 | data[..., :-1][white_areas.T] = (128, 128, 255) # Replace values that fullfill the condition with 128,128,255 1464 | # white_areas = (red == 1) & (blue == 1) & (green == 1) 1465 | # data[..., :-1][white_areas.T] = (128, 128, 255) # Transpose back needed 1466 | im2 = Image.fromarray(data) 1467 | im2 = im2.convert('RGB') 1468 | im.close() 1469 | im2.save(fileToPerformPostProcessOn) 1470 | im2.close() 1471 | # im2.show() 1472 | # print("Second hello") 1473 | bpy.context.preferences.studio_lights[0].name 1474 | 1475 | 1476 | 1477 | 1478 | 1479 | @renderPassDecorator 1480 | class RenderNormalPassWithWorkbench(RenderPass): 1481 | passName = "Normal" # @bookmark OBS! Im doing something funky in perform_post_process where I alter this mid.script on the fly, you will need to reflect a change to this pass name 1482 | # where I restore it inside of perform_post_process. This only applies to the normalRenderPass as unlike other passes it makes a second render in the post process method. 1483 | 1484 | 1485 | @classmethod 1486 | def prepare_to_render(self): 1487 | 1488 | 1489 | # Output Properties > Output 1490 | bpy.context.scene.render.image_settings.color_mode = 'RGB' 1491 | bpy.context.scene.render.image_settings.file_format = 'PNG' 1492 | bpy.context.scene.render.image_settings.color_depth = '8' 1493 | bpy.context.scene.render.image_settings.compression = 60 1494 | bpy.context.scene.render.use_overwrite = True 1495 | 1496 | 1497 | 1498 | 1499 | # Worlds properties 1500 | if not nameOfWorldUsedDuringPBRmapCreation in bpy.data.worlds: 1501 | bpy.data.worlds.new(nameOfWorldUsedDuringPBRmapCreation) 1502 | 1503 | # bpy.data.worlds 1504 | bpy.context.scene.world = bpy.data.worlds[nameOfWorldUsedDuringPBRmapCreation] 1505 | # bpy.context.scene.world.color = (0.215861, 0.215861, 1) 1506 | # bpy.context.scene.world.color = (0.215861*2, 0.215861*2, 1) 1507 | # bpy.context.scene.world.color = (0.385001*2, 0.385001*2, 1) 1508 | bpy.context.scene.world.color = (0.5, 0.5, 1) 1509 | # bpy.context.scene.world. 1510 | 1511 | # bpy.context.world = bpy.contex 1512 | 1513 | 1514 | # Switch viewport view to rendered, rather than solid or wire or whatever. 1515 | bpy.context.space_data.shading.type = 'RENDERED' 1516 | 1517 | # Hides things like the 3D cursor, object selection outlines (etc). 1518 | bpy.context.space_data.overlay.show_overlays = False 1519 | 1520 | # Set the render engine to workbench rather than Eevee/Cycles 1521 | bpy.context.scene.render.engine = 'BLENDER_WORKBENCH' 1522 | 1523 | # Render properties > Lighting 1524 | bpy.context.scene.display.shading.light = 'MATCAP' 1525 | bpy.context.scene.display.shading.studio_light = 'FastPBRNormalMatCap.exr' 1526 | # bpy.context.scene.display.shading.studio_light = 'check_normal+y.exr' 1527 | 1528 | # Render properties > Color 1529 | bpy.context.scene.display.shading.color_type = 'SINGLE' 1530 | bpy.context.scene.display.shading.single_color = (1, 1, 1) 1531 | 1532 | # Render properties > Options 1533 | bpy.context.scene.display.shading.show_backface_culling = False 1534 | bpy.context.scene.display.shading.show_xray = False 1535 | bpy.context.scene.display.shading.show_shadows = False 1536 | bpy.context.scene.display.shading.show_cavity = False 1537 | bpy.context.scene.display.shading.use_dof = False 1538 | bpy.context.scene.display.shading.show_object_outline = False 1539 | bpy.context.scene.display.shading.show_specular_highlight = True 1540 | 1541 | # Render properties > Color management 1542 | bpy.context.scene.display_settings.display_device = 'None' 1543 | bpy.context.scene.view_settings.view_transform = 'Standard' 1544 | bpy.context.scene.view_settings.look = 'None' 1545 | bpy.context.scene.view_settings.exposure = 0 1546 | bpy.context.scene.view_settings.gamma = 1 1547 | bpy.context.scene.sequencer_colorspace_settings.name = 'Raw' 1548 | 1549 | # Render properties > Sampling 1550 | bpy.context.scene.eevee.taa_samples = 1 1551 | bpy.context.scene.eevee.taa_render_samples = 1 1552 | 1553 | # @bookmark _Debugsession 66 THE ISSUE IS IN THIS FUNCTION 1554 | @classmethod 1555 | def perform_post_process(cls): 1556 | # return 1557 | # Now we will invert the pixels where the normal map has rendered a face that is facing away from the camera. 1558 | 1559 | 1560 | 1561 | # This will let users simply not care about correcting there face normals before rendering, so even if it may 1562 | # add 3-4 seconds worth of render time - its probably gonna save the user more time than if he had to ensure 1563 | # his normals are correct. 1564 | pathToStoreImageInIncludingFileNameAndFileExtension = pathToStoreImagesIn 1565 | fileToPerformPostProcessOn = pathToStoreImageInIncludingFileNameAndFileExtension + cls.passName + ".png" 1566 | 1567 | imageWithoutCull = Image.open(fileToPerformPostProcessOn).convert('RGB') 1568 | # import numpy as np 1569 | bpy.context.scene.display.shading.show_backface_culling = True 1570 | cls.render() 1571 | cls.passName = 'temp' 1572 | cls.save_to_disk() # Save a 'temp' file to disk. 1573 | normalWithBackfaceCulling = pathToStoreImageInIncludingFileNameAndFileExtension + cls.passName + ".png" 1574 | # imageWithCull = Image.open(normalWithBackfaceCulling).convert('RGB') 1575 | imageWithCull = Image.open(normalWithBackfaceCulling) 1576 | print(f"!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! imageWithCull: {imageWithCull.__str__()}") 1577 | # imageWithCull = Image.open('C:/Users/Oliver/Desktop/test/inversionTest/withCull.png').convert('RGB') 1578 | # imageWithoutCull = Image.open('C:/Users/Oliver/Desktop/test/inversionTest/withoutCull.png').convert('RGB') 1579 | # img1 = Image.open('C:/Users/Oliver/Desktop/test/untitled.png').convert('RGB') 1580 | # img2 = Image.open('C:/Users/Oliver/Desktop/test/untitled3.png').convert('RGB') 1581 | 1582 | # # image3 = ImageChops.difference(image1, image2) 1583 | 1584 | # # image3.save('C:/Users/Oliver/Desktop/test/untitled3.png') 1585 | 1586 | 1587 | # # from PIL import Image, ImageChops 1588 | # print(img1) 1589 | # # assign images 1590 | # # img1 = Image.open("1img.jpg") 1591 | # # img2 = Image.open("2img.jpg") 1592 | # # image1.show() 1593 | # # finding difference 1594 | # # import numpy as np 1595 | # # whiteImage = np.zeros([100,100,3],dtype=np.uint8) 1596 | # # whiteImage.fill(255) # or img[:] = 255 1597 | # whiteImage = Image.new('RGB', (1920, 1080), color = (1,1,1)) 1598 | # diff = ImageChops.add_modulo(ImageChops.difference(img1, img2).convert('RGB'), whiteImage).convert('L') 1599 | # # ImageChops.hard_light 1600 | # final = Image.composite(img2, img1, diff) 1601 | # # showing the difference 1602 | # ImageChops.hard_light 1603 | 1604 | # img1AsArray = numpy.asarray(img1) 1605 | # img2AsArray = numpy.asarray(img2) 1606 | 1607 | # final = PIL.Image.fromarray(numpy.uint8((img1AsArray == img2AsArray))) 1608 | 1609 | 1610 | # final.show() 1611 | 1612 | # import numpy as np 1613 | print(imageWithoutCull.size) 1614 | # img1 = Image.open('test.png') 1615 | imageWithoutCull = imageWithoutCull.convert('RGBA') 1616 | imageWithCull = imageWithCull.convert('RGBA') 1617 | 1618 | imageWithoutCullAsArray = numpy.array(imageWithoutCull) # "data" is a height x width x 4 numpy array 1619 | imageWithCullAsArray = numpy.array(imageWithCull) # "data" is a height x width x 4 numpy array 1620 | red1, green1, blue1, alpha1 = imageWithoutCullAsArray.T # Temporarily unpack the bands for readability 1621 | red2, green2, blue2, alpha2 = imageWithCullAsArray.T # Temporarily unpack the bands for readability 1622 | 1623 | # Replace white with red... (leaves alpha values alone...) 1624 | differences = (red1 == red2) & (blue1 == blue2) & (green1 == green2) 1625 | # image1asArray = Image.new('RGBA', (1920, 1080), (0,0,0,0)) 1626 | imageWithoutCullAsArray[..., :-1][differences.T] = (0, 0, 0) # Transpose back needed 1627 | imageWithoutCullAsArray[..., :-1][~ differences.T] = (255, 255, 255) # Transpose back needed 1628 | 1629 | diff = Image.fromarray(imageWithoutCullAsArray).convert('L') 1630 | 1631 | red, green, blue, alpha = imageWithoutCull.split() 1632 | invertedImg1 = Image.merge('RGBA', (ImageChops.invert(red), ImageChops.invert(green), blue, alpha)) 1633 | 1634 | imageWithoutCull = imageWithoutCull.convert('RGB') 1635 | invertedImg1 = invertedImg1.convert('RGB') 1636 | final = Image.composite(invertedImg1, imageWithoutCull, diff) 1637 | imageWithoutCull.close() 1638 | invertedImg1.close() 1639 | diff.close() 1640 | imageWithCull.close() 1641 | final.save(fileToPerformPostProcessOn) 1642 | final.close() 1643 | 1644 | # from PIL import Image 1645 | # from PIL.ImageChops import invert 1646 | 1647 | # image = Image.open('test.tif') 1648 | # red, green, blue = image.split() 1649 | # image_with_inverted_green = Image.merge('RGB', (invert(red), invert(green), blue)) 1650 | # image_with_inverted_green.save('test_inverted_green.tif') 1651 | 1652 | os.remove(normalWithBackfaceCulling) 1653 | bpy.context.scene.display.shading.show_backface_culling = False 1654 | cls.passName = 'Normal' # @bookmark OBS! Im doing something very funky here with changing the passName in the middle of a function like this 1655 | # Im doing it for the purpose of being able to render a second image for a specific pass, but the system wasnt really designed for this so Im hijacking 1656 | # it slightly. Potentially bugprone if you start copy pasting code as you will need to declare pass names in 2 places for methods that changes it 1657 | # like this, but on this particular line and where you normall do at the beginning of the renderPass child class. 1658 | 1659 | # @bookmark Curvature pass 1660 | @renderPassDecorator 1661 | class RenderCurvaturePassWithWorkbench(RenderPass): 1662 | passName = "Curvature" 1663 | 1664 | @classmethod 1665 | def prepare_to_render(self): 1666 | # # Switch viewport view to rendered, rather than solid or wire or whatever. 1667 | # bpy.context.space_data.shading.type = 'RENDERED' 1668 | 1669 | # # Hides things like the 3D cursor, object selection outlines (etc). 1670 | # bpy.context.space_data.overlay.show_overlays = False 1671 | 1672 | # # Set the render engine to workbench rather than Eevee/Cycles 1673 | # bpy.context.scene.render.engine = 'BLENDER_WORKBENCH' 1674 | 1675 | # bpy.data.worlds 1676 | bpy.context.scene.world = bpy.data.worlds[nameOfWorldUsedDuringPBRmapCreation] 1677 | bpy.context.scene.world.color = (0.5, 0.5, 0.5) 1678 | 1679 | # Render properties > Lighting 1680 | bpy.context.scene.display.shading.light = 'FLAT' 1681 | 1682 | 1683 | 1684 | # Render properties > Color 1685 | bpy.context.scene.display.shading.color_type = 'SINGLE' 1686 | bpy.context.scene.display.shading.single_color = (0.5, 0.5, 0.5) 1687 | bpy.context.scene.display.shading.cavity_valley_factor = 2.5 1688 | bpy.context.scene.display.shading.cavity_ridge_factor = 2.5 1689 | 1690 | 1691 | 1692 | # # Render properties > Options 1693 | bpy.context.scene.display.shading.show_cavity = True 1694 | bpy.context.scene.display.shading.cavity_type = 'WORLD' 1695 | # bpy.context.scene.display.shading.show_backface_culling = False 1696 | # bpy.context.scene.display.shading.show_xray = False 1697 | # bpy.context.scene.display.shading.show_shadows = False 1698 | # bpy.context.scene.display.shading.show_cavity = True 1699 | # bpy.context.scene.display.shading.use_dof = False 1700 | # bpy.context.scene.display.shading.show_object_outline = False 1701 | # bpy.context.scene.display.shading.show_specular_highlight = True 1702 | 1703 | @renderPassDecorator 1704 | class RenderMatIDPassWithWorkbenchViewportDisplayCol(RenderPass): 1705 | passName = "MatID" 1706 | 1707 | 1708 | display_device = '' 1709 | view_transform = '' 1710 | sequencer_colorspace_settings_name = '' 1711 | 1712 | 1713 | @classmethod 1714 | def prepare_to_render(self): 1715 | self: RenderMatIDPassWithWorkbenchViewportDisplayCol 1716 | 1717 | # # 3D Viewport > Viewport shading (top right) > Render Pass 1718 | # bpy.context.space_data.shading.render_pass = 'DIFFUSE_COLOR' 1719 | 1720 | # # Render properties > Sampling 1721 | # bpy.context.scene.eevee.taa_samples = 1 1722 | 1723 | # Render properties > Lighting 1724 | bpy.context.scene.display.shading.color_type = 'MATERIAL' 1725 | 1726 | # Render properties > Color 1727 | bpy.context.scene.display.shading.light = 'FLAT' 1728 | 1729 | # Render properties > Options 1730 | bpy.context.scene.display.shading.show_cavity = False 1731 | 1732 | # Render propertties > Sampling 1733 | bpy.context.scene.display.viewport_aa = 'OFF' 1734 | bpy.context.scene.display.render_aa = 'OFF' 1735 | 1736 | 1737 | 1738 | 1739 | # Render Properties > Color Management 1740 | self.display_device = bpy.context.scene.display_settings.display_device 1741 | bpy.context.scene.display_settings.display_device = 'sRGB' 1742 | 1743 | self.view_transform = bpy.context.scene.view_settings.view_transform 1744 | bpy.context.scene.view_settings.view_transform = 'Filmic' 1745 | 1746 | self.sequencer_colorspace_settings_name = bpy.context.scene.sequencer_colorspace_settings.name 1747 | bpy.context.scene.sequencer_colorspace_settings.name = 'Filmic Log' 1748 | 1749 | 1750 | # for material in bpy.data.materials: 1751 | # material: bpy.types.Material 1752 | # material.diffuse_color = material.node_tree.nodes 1753 | # bpy.data.materials["Material"].node_tree.nodes["Principled BSDF"].inputs[0].default_value = (0.8, 0.272124, 0.414903, 1) 1754 | 1755 | @classmethod 1756 | def perform_post_process(self): 1757 | bpy.context.scene.display_settings.display_device = self.display_device 1758 | bpy.context.scene.view_settings.view_transform = self.view_transform 1759 | bpy.context.scene.sequencer_colorspace_settings.name = self.sequencer_colorspace_settings_name 1760 | 1761 | 1762 | # @bookmark AO pass 1763 | @renderPassDecorator 1764 | class RenderAOPassWithEevee(RenderPass): 1765 | passName = "AO" 1766 | 1767 | @classmethod 1768 | def prepare_to_render(self): 1769 | # Render properties > Render Engine 1770 | bpy.context.scene.render.engine = 'BLENDER_EEVEE' 1771 | 1772 | # Render properties > Ambient Occlusion 1773 | bpy.context.scene.eevee.use_gtao = True 1774 | bpy.context.scene.eevee.gtao_distance = 7 1775 | bpy.context.scene.eevee.gtao_factor = 1.3 1776 | 1777 | 1778 | 1779 | 1780 | # 3D Viewport > Viewport shading (top right) > Render Pass 1781 | bpy.context.space_data.shading.render_pass = 'AO' 1782 | 1783 | 1784 | # Render properties > Sampling 1785 | bpy.context.scene.eevee.taa_samples = 32 1786 | 1787 | @renderPassDecorator 1788 | class RenderHeightPassWithEeveeMist(RenderPass): 1789 | passName = "Height" 1790 | 1791 | @classmethod 1792 | def prepare_to_render(self): 1793 | # Render properties > Render Engine 1794 | bpy.context.scene.render.engine = 'BLENDER_EEVEE' 1795 | 1796 | # 3D Viewport > Viewport shading (top right) > Render Pass 1797 | bpy.context.space_data.shading.render_pass = 'MIST' 1798 | 1799 | # Render properties > Sampling 1800 | bpy.context.scene.eevee.taa_samples = 1 1801 | 1802 | @renderPassDecorator 1803 | class RenderMatIDPassWithEevee(RenderPass): 1804 | passName = "MatID" 1805 | 1806 | @classmethod 1807 | def prepare_to_render(self): 1808 | 1809 | # 3D Viewport > Viewport shading (top right) > Render Pass 1810 | bpy.context.space_data.shading.render_pass = 'DIFFUSE_COLOR' 1811 | 1812 | # Render properties > Sampling 1813 | bpy.context.scene.eevee.taa_samples = 1 1814 | 1815 | # @bookmark Transparency pass 1816 | 1817 | # Supports "Alpha hashed" & "alpha clipped", blend mode, however "alpha blend" 1818 | # will appear fully transparent, which might be undesired. 1819 | @renderPassDecorator 1820 | class RenderTransparencyPassWithEeveeEnvironmentPass(RenderPass): 1821 | passName = "Transparency" 1822 | 1823 | # settings = dict() 1824 | # settings["High quality transparency"] = False 1825 | 1826 | restoreDisplayDevice = '' 1827 | restoreViewtransform = '' 1828 | restoreSequencer = '' 1829 | 1830 | 1831 | @classmethod 1832 | def prepare_to_render(self): 1833 | 1834 | # 3D Viewport > Viewport shading (top right) > Render Pass 1835 | bpy.context.space_data.shading.render_pass = 'ENVIRONMENT' 1836 | 1837 | # Render properties > Sampling 1838 | # if self.settings["High quality transparency"]: 1839 | # bpy.context.scene.eevee.taa_samples = 256 1840 | # else: 1841 | bpy.context.scene.eevee.taa_samples = 1 1842 | 1843 | # Render properties > Color 1844 | bpy.context.scene.display.shading.single_color = (999, 999, 999) 1845 | 1846 | # World properties > Surface > color 1847 | bpy.context.scene.world.color = (9999, 9999, 9999) 1848 | 1849 | 1850 | self.restoreDisplayDevice = bpy.context.scene.display_settings.display_device 1851 | bpy.context.scene.display_settings.display_device = 'None' 1852 | 1853 | # self.restoreViewtransform = bpy.context.scene.view_settings.view_transform 1854 | # bpy.context.scene.view_settings.view_transform = 'Raw' 1855 | 1856 | 1857 | self.restoreSequencer = bpy.context.scene.sequencer_colorspace_settings.name 1858 | bpy.context.scene.sequencer_colorspace_settings.name = 'Raw' 1859 | 1860 | 1861 | 1862 | 1863 | @classmethod 1864 | def perform_post_process(self): 1865 | # Inverts the alpha pass so that white means visible and black means invisible. 1866 | bpy.context.scene.display_settings.display_device = self.restoreDisplayDevice 1867 | # bpy.context.scene.view_settings.view_transform = self.restoreViewtransform 1868 | bpy.context.scene.sequencer_colorspace_settings.name = self.restoreSequencer 1869 | 1870 | pathToStoreImageInIncludingFileNameAndFileExtension = pathToStoreImagesIn 1871 | fileToPerformPostProcessOn = pathToStoreImageInIncludingFileNameAndFileExtension + self.passName + ".png" 1872 | imageToPerformPostProcessOn = Image.open(fileToPerformPostProcessOn) 1873 | finalImage = ImageChops.invert(imageToPerformPostProcessOn) 1874 | imageToPerformPostProcessOn.close() 1875 | finalImage.save(fileToPerformPostProcessOn) 1876 | 1877 | 1878 | 1879 | # def RenderNormalFromAppendedMaterialDecorator(cls: RenderAppendedMaterialTypeRenderPass): 1880 | # retrieveSettings(cls).enable_pass = False 1881 | 1882 | # @RenderNormalFromAppendedMaterialDecorator 1883 | # @renderPassDecorator 1884 | # class RenderNormalFromAppendedMaterial(RenderAppendedMaterialTypeRenderPass): 1885 | # passName = "normal" 1886 | # materialToRender = 'FastPBRNormal' 1887 | # def draw_pass_specific_ui_elements(cls, context): 1888 | # label_multiline(f"""Compared to rendering the normal pass with workbench, this pass has the advantage of that it totally ignores whether faces are pointing the wrong direction or not, they will always appear like as if they were pointing towards the camera, or a direction that has a maximum of a 90 degree angle from the camera. A downside of this pass is that it might not be able to replace the material of all objects if you have objects from other files linked in your blend.""",context,cls.layout) 1889 | 1890 | 1891 | ################################################################################### 1892 | ######################### END OF PASSES SECTION ################################### 1893 | ################################################################################### 1894 | 1895 | 1896 | 1897 | 1898 | 1899 | 1900 | # class CustomDrawOperator(bpy.types.Operator): 1901 | # bl_idname = "object.custom_draw" 1902 | # bl_label = "Simple Modal Operator" 1903 | 1904 | # filepath: bpy.props.StringProperty(subtype="FILE_PATH") 1905 | 1906 | # my_float: bpy.props.FloatProperty(name="Float") 1907 | # my_bool: bpy.props.BoolProperty(name="Toggle Option") 1908 | # my_string: bpy.props.StringProperty(name="String Value") 1909 | 1910 | # def execute(self, context): 1911 | # print("Test", self) 1912 | # return {'FINISHED'} 1913 | 1914 | # def invoke(self, context, event): 1915 | # wm = context.window_manager.invoke_props_dialog(self) 1916 | # return wm.invoke_props_dialog(self) 1917 | 1918 | # bpy.types. 1919 | 1920 | # def test123(cls): 1921 | # print(cls + "AWESOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOME") 1922 | 1923 | # @bookmark Operator Base Class 1924 | class OperatorBaseClass(bpy.types.Operator, object): 1925 | __metaclass__ = registerOperatorsDecorator # This method runs when the class is created - Edit: Appears broken? 1926 | # Currently still calling the function as a decorator on a per child-class basis! 1927 | bl_idname = "" 1928 | bl_label = "" # Display name in the interface. 1929 | bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator. 1930 | 1931 | 1932 | 1933 | 1934 | 1935 | # class test(PluginBase): 1936 | # pass 1937 | 1938 | @registerOperatorsDecorator 1939 | class FastPBRBackupSettings(OperatorBaseClass): 1940 | def execute(self, context: bpy.types.Context) -> bpy.typing.Set[str]: 1941 | BackupPrepareAndRestore.backupSettingsAndPrepareForRender() 1942 | return {'FINISHED'} 1943 | 1944 | 1945 | @registerOperatorsDecorator 1946 | class FastPBRRestoreSettings(OperatorBaseClass): 1947 | def execute(self, context: bpy.types.Context) -> bpy.typing.Set[str]: 1948 | BackupPrepareAndRestore.restoreSettings() 1949 | return {'FINISHED'} 1950 | 1951 | # @bookmark MatchViewportDisplayAndSurfaceBaseColor Operator 1952 | 1953 | replaceSelectedObjectMaterials = 'MaterialsOnSelectedObjectsOnly' 1954 | replaceScene = 'AllMaterialsInTheCurrentScene' 1955 | replaceAll = 'AllMaterialsInTheCurrentBlend' 1956 | 1957 | @settingsContainerDecorator 1958 | class OperatorSettingsContainer(): 1959 | class settings(bpy.types.PropertyGroup): 1960 | materials_to_replace: bpy.props.EnumProperty(name = "My Property", items={(replaceSelectedObjectMaterials,replaceSelectedObjectMaterials,replaceSelectedObjectMaterials), 1961 | (replaceScene,replaceScene,replaceScene), 1962 | (replaceAll,replaceAll,replaceAll)}, default = replaceSelectedObjectMaterials) = propertyClass() 1963 | 1964 | 1965 | 1966 | 1967 | @registerOperatorsDecorator 1968 | class MatchViewportDisplayAndSurfaceBaseColor(OperatorBaseClass): 1969 | 1970 | # class settings(bpy.types.PropertyGroup): 1971 | # materials_to_replace: bpy.props.EnumProperty(name = "My Property", items={(replaceSelectedObjectMaterials,replaceSelectedObjectMaterials,replaceSelectedObjectMaterials), 1972 | # (replaceScene,replaceScene,replaceScene), 1973 | # (replaceAll,replaceAll,replaceAll)}, default = replaceSelectedObjectMaterials) 1974 | 1975 | 1976 | 1977 | # flag_prop: bpy.props.BoolProperty(name = "Use Int") 1978 | # dependent_prop: bpy.props.EnumProperty(name = "My Property", items={(replaceSelectedObjectMaterials,replaceSelectedObjectMaterials,replaceSelectedObjectMaterials), 1979 | # (replaceScene,replaceScene,replaceScene), 1980 | # (replaceAll,replaceAll,replaceAll)}, default = 'Foo') 1981 | 1982 | def execute(self, context): 1983 | msg = f"dependent_prop: {retrieveSettings(OperatorSettingsContainer).materials_to_replace}" 1984 | self.report({'INFO'}, msg) 1985 | print(msg) 1986 | 1987 | materialsToReplace: list(bpy.types.Material) = list() 1988 | if retrieveSettings(OperatorSettingsContainer).materials_to_replace == replaceAll: 1989 | materialsToReplace = bpy.data.materials 1990 | elif retrieveSettings(OperatorSettingsContainer).materials_to_replace in (replaceSelectedObjectMaterials, replaceScene): 1991 | if retrieveSettings(OperatorSettingsContainer).materials_to_replace == replaceSelectedObjectMaterials: 1992 | objectsToLoop = bpy.context.selected_objects 1993 | elif retrieveSettings(OperatorSettingsContainer).materials_to_replace == replaceScene: 1994 | objectsToLoop = bpy.context.scene.objects 1995 | 1996 | for object in objectsToLoop: 1997 | object: bpy.types.Object 1998 | for materialSlot in object.material_slots: 1999 | materialSlot: bpy.types.MaterialSlot 2000 | materialsToReplace.append(materialSlot.material) 2001 | # elif self.dependent_prop == self.replaceScene: 2002 | for material in materialsToReplace: 2003 | material: bpy.types.Material 2004 | if material.use_nodes: 2005 | if material.node_tree.nodes.find("Principled BSDF") > -1: 2006 | material.diffuse_color = material.node_tree.nodes["Principled BSDF"].inputs[0].default_value 2007 | # bpy.data.materials["!@darkGray"].node_tree.nodes["Principled BSDF"].inputs[0].default_value 2008 | return {'FINISHED'} 2009 | 2010 | def draw(self, context): 2011 | self.layout.use_property_split = True 2012 | 2013 | row = self.layout.row() 2014 | # row.prop(self, "flag_prop") 2015 | 2016 | sub = row.row() 2017 | # sub.enabled = self.flag_prop 2018 | sub.prop(retrieveSettings(OperatorSettingsContainer), 'materials_to_replace', text="MaterialsToReplace") 2019 | # sub.prop(retrieveSettings(OperatorSettingsContainer), OperatorSettingsContainer.settings.materials_to_replace.__name__, text="Materials to replace: ") 2020 | # sub.prop(self, "materials_to_replace", text="Materials to replace: ") 2021 | 2022 | # Backup: 2023 | 2024 | # replaceSelectedObjectMaterials = 'MaterialsOnSelectedObjectsOnly' 2025 | # replaceScene = 'AllMaterialsInTheCurrentScene' 2026 | # replaceAll = 'AllMaterialsInTheCurrentBlend' 2027 | 2028 | # @settingsContainerDecorator 2029 | # @registerOperatorsDecorator 2030 | # class MatchViewportDisplayAndSurfaceBaseColor(OperatorBaseClass): 2031 | 2032 | # class settings(bpy.types.PropertyGroup): 2033 | # materials_to_replace: bpy.props.EnumProperty(name = "My Property", items={(replaceSelectedObjectMaterials,replaceSelectedObjectMaterials,replaceSelectedObjectMaterials), 2034 | # (replaceScene,replaceScene,replaceScene), 2035 | # (replaceAll,replaceAll,replaceAll)}, default = 'Foo') 2036 | 2037 | 2038 | 2039 | # # flag_prop: bpy.props.BoolProperty(name = "Use Int") 2040 | # # dependent_prop: bpy.props.EnumProperty(name = "My Property", items={(replaceSelectedObjectMaterials,replaceSelectedObjectMaterials,replaceSelectedObjectMaterials), 2041 | # # (replaceScene,replaceScene,replaceScene), 2042 | # # (replaceAll,replaceAll,replaceAll)}, default = 'Foo') 2043 | 2044 | # def execute(self, context): 2045 | # msg = f"dependent_prop: {self.dependent_prop}" if self.flag_prop else "Nothing to report" 2046 | # self.report({'INFO'}, msg) 2047 | # print(msg) 2048 | 2049 | # materialsToReplace: list(bpy.types.Material) = list() 2050 | # if self.dependent_prop == self.replaceAll: 2051 | # materialsToReplace = bpy.data.materials 2052 | # elif self.dependent_prop in (self.replaceSelectedObjectMaterials, self.replaceScene): 2053 | # if self.dependent_prop == self.replaceSelectedObjectMaterials: 2054 | # objectsToLoop = bpy.context.selected_objects 2055 | # elif self.dependent_prop == self.replaceScene: 2056 | # objectsToLoop = bpy.context.scene.objects 2057 | 2058 | # for object in objectsToLoop: 2059 | # object: bpy.types.Object 2060 | # for materialSlot in object.material_slots: 2061 | # materialSlot: bpy.types.MaterialSlot 2062 | # materialsToReplace.append(materialSlot.material) 2063 | # # elif self.dependent_prop == self.replaceScene: 2064 | # for material in materialsToReplace: 2065 | # material: bpy.types.Material 2066 | # if material.use_nodes: 2067 | # if material.node_tree.nodes.find("Principled BSDF") > -1: 2068 | # material.diffuse_color = material.node_tree.nodes["Principled BSDF"].inputs[0].default_value 2069 | # # bpy.data.materials["!@darkGray"].node_tree.nodes["Principled BSDF"].inputs[0].default_value 2070 | # return {'FINISHED'} 2071 | 2072 | # def draw(self, context): 2073 | # self.layout.use_property_split = True 2074 | 2075 | # row = self.layout.row() 2076 | # row.prop(self, "flag_prop") 2077 | 2078 | # sub = row.row() 2079 | # # sub.enabled = self.flag_prop 2080 | # sub.prop(self, "dependent_prop", text="Materials to replace: ") 2081 | 2082 | #### backup end 2083 | 2084 | 2085 | 2086 | # def execute(self, context): # execute() is called when running the operator. 2087 | # pass 2088 | 2089 | 2090 | # MENU REFERENCE: 2091 | 2092 | 2093 | # import bpy 2094 | 2095 | # class VIEW3D_MT_menu(bpy.types.Menu): 2096 | # bl_label = "Test" 2097 | 2098 | # def draw(self, context): 2099 | # self.layout.operator("mesh.primitive_monkey_add") 2100 | # self.layout.menu("OBJECT_MT_select_submenu") 2101 | 2102 | # def addmenu_callback(self, context): 2103 | # self.layout.menu("VIEW3D_MT_menu") 2104 | 2105 | 2106 | # def register(): 2107 | # bpy.utils.register_class(VIEW3D_MT_menu) 2108 | # bpy.types.VIEW3D_MT_editor_menus.append(addmenu_callback) 2109 | 2110 | # def unregister(): 2111 | # bpy.types.VIEW3D_MT_editor_menus.remove(addmenu_callback) 2112 | # bpy.utils.unregister_class(VIEW3D_MT_menu) 2113 | 2114 | 2115 | # if __name__ == "__main__": 2116 | # register() 2117 | 2118 | 2119 | # import bpy 2120 | 2121 | 2122 | 2123 | # class SubMenu(bpy.types.Menu): 2124 | # bl_idname = "OBJECT_MT_select_submenu" 2125 | # bl_label = "Select" 2126 | 2127 | # def draw(self, context): 2128 | # layout = self.layout 2129 | 2130 | # # layout.operator("object.select_all", text="Select/Deselect All").action = 'TOGGLE' 2131 | # # layout.operator("object.select_all", text="Inverse").action = 'INVERT' 2132 | # # layout.operator("object.select_random", text="Random") 2133 | 2134 | # # # access this operator as a submenu 2135 | # # layout.operator_menu_enum("object.select_by_type", "type", text="Select All by Type...") 2136 | 2137 | # # layout.separator() 2138 | 2139 | # # # expand each operator option into this menu 2140 | # # layout.operator_enum("object.lamp_add", "type") 2141 | 2142 | # # layout.separator() 2143 | 2144 | # # # use existing memu 2145 | # self.layout.menu("VIEW3D_MT_transform") 2146 | 2147 | 2148 | # bpy.utils.register_class(SubMenu) 2149 | 2150 | # # test call to display immediately. 2151 | # bpy.ops.wm.call_menu(name="OBJECT_MT_select_submenu") 2152 | 2153 | # 2154 | 2155 | # bpy.types.VIEW3D_MT_editor_menus.append(addmenu_callback) 2156 | 2157 | # MENU REFERENCE END 2158 | 2159 | ############################### 2160 | # @bookmark Menus and headers # 2161 | ############################### 2162 | # menusToRegister = dict() 2163 | 2164 | # @registerOperatorsDecorator 2165 | # # class Fast(FastToolBarPanelBaseClass, bpy.types.Panel): 2166 | # class Fast(bpy.types.Menu): 2167 | # menusAndHeadersToRegisterUnder = list() 2168 | # menusAndHeadersToRegisterUnder.append('VIEW3D_MT_editor_menus') 2169 | # # bl_space_type = "VIEW_3D" 2170 | # # bl_region_type = "NAVIGATION_BAR" 2171 | # # bl_category = "Object" 2172 | # # bl_region_type = "TOOLS" # This actually puts the panel 2173 | # # on the left side of the 3D viewport! Since the left panel is usually very 2174 | # # narrow however, I dont recommend putting it here. 2175 | # def draw(self, context: bpy.types.Context) -> None: 2176 | # self.layout.row().prop(retrieveOperatorFromCls(MatchViewportDisplayAndSurfaceBaseColor)) 2177 | 2178 | 2179 | # def registerMenusDecorator(cls: FastMenuBaseClass): 2180 | # # registerOperatorsDecorator(cls) 2181 | # registerClassGivebl_labelAndbl_idnameWithUnderscore(cls) 2182 | # # menusToRegister.append((cls, cls.menusAndHeadersToRegisterUnder)) 2183 | # menusToRegister[cls] = cls.menusAndHeadersToRegisterUnder 2184 | 2185 | 2186 | 2187 | # # def addmenu_callback(self, context): 2188 | # # self.layout.menu("VIEW3D_MT_menu") 2189 | 2190 | 2191 | 2192 | # class FastMenuBaseClass(bpy.types.Menu): 2193 | # # pass 2194 | # menusAndHeadersToRegisterUnder = list() 2195 | # menusAndHeadersToRegisterUnder.append('VIEW3D_MT_editor_menus') 2196 | # # menusAndHeadersToRegisterUnder.append(retrieveOperatorFromCls(Fast).replace('.', '_')) 2197 | 2198 | # @registerMenusDecorator 2199 | # class Materials(FastMenuBaseClass): 2200 | # def draw(self, context: bpy.types.Context) -> None: 2201 | # self.layout.operator(retrieveOperatorFromCls(MatchViewportDisplayAndSurfaceBaseColor)) 2202 | 2203 | ################################# 2204 | # # # Menus and headers END # # # 2205 | ################################# 2206 | 2207 | # @bookmark Fast PBR Viewport Render opeartor 2208 | @registerOperatorsDecorator 2209 | class FastPBRViewportRender(OperatorBaseClass): 2210 | 2211 | # print("Auto generated bl_idname based on the class name!!" + bl_idname) 2212 | # bl_description = """fast pbr viewport render""" # Use this as a tooltip for menu items and buttons. 2213 | # bl_idname = "object.fast_pbr_viewport_render" # Unique identifier for buttons and menu items to reference. 2214 | # bl_idname = "fast_pbr.fast_pbr_viewport_render" # Unique identifier for buttons and menu items to reference. 2215 | # bl_label = "Fast PBR viewport render" # Display name in the interface. 2216 | # bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator. 2217 | 2218 | 2219 | 2220 | def test(self): 2221 | print("wot") 2222 | def execute(self, context): # execute() is called when running the operator. 2223 | #os.system("cls") 2224 | # The original script 2225 | 2226 | # print(context.window_manager.invoke_props_dialog(self.test)) 2227 | # for renderPassClass in 2228 | 2229 | # renderNormalPassWithWorkbench = RenderNormalPassWithWorkbench() 2230 | # renderNormalPassWithWorkbench.prepare_render_and_save() 2231 | 2232 | # print("HERE:",retrieveOperatorFromCls(FastPBRBackupSettings)) 2233 | BackupPrepareAndRestore.backupSettingsAndPrepareForRender() 2234 | 2235 | # bpy.ops.wm.append( 2236 | # filepath="cube.blend", 2237 | # directory="/home/lucas/Desktop/cube.blend\\Object\\", 2238 | # filename="Cube") 2239 | 2240 | ################################################## 2241 | # import pathlib 2242 | # pathToAddonDirectory = str(pathlib.Path(__file__).parent.resolve()) 2243 | # nameOfAssetsFileWithoutPath = 'FastPBRAssets.blend' 2244 | # pathToAssetsFile = pathToAddonDirectory + '/' + nameOfAssetsFileWithoutPath 2245 | # # 2246 | # def appendAssetFromAssetsBlendFile(dataBlockToAppend: str, blendFileDataCategory: str): 2247 | # """ 2248 | # dataBlockToAppend is the name of the object/material/whatever you want to append. 2249 | 2250 | # blendFileDataCategory is the type of data you want to append, if you go into the outliner > display mode > Data API you will see all these categories. Example values: "Material", "Object", "Node Groups" (etc). For some reason, the "s" at the end of some categories displayed in that list is not supposed to be included, which is rather confusing :) 2251 | # """ 2252 | # bpy.ops.wm.append(filename=dataBlockToAppend, directory=pathToAssetsFile + '\\' + blendFileDataCategory + '\\') 2253 | 2254 | # print("filepath:", nameOfAssetsFileWithoutPath) 2255 | # print("directory:", pathToAssetsFile + '\\' + blendFileDataCategory) 2256 | # print("filename:", dataBlockToAppend) 2257 | 2258 | # pathToNormalAssetsFile = pathToAddonDirectory + '/' + 'FastPBRAssets.blend' 2259 | # print("File path:", pathlib.Path(__file__).parent.resolve()) 2260 | 2261 | # if bpy.data.materials.find('FastPBRNormal') < 0: 2262 | # appendAssetFromAssetsBlendFile('FastPBRNormal', 'Material') 2263 | # for object in bpy.data.objects: 2264 | # object: bpy.types.Object 2265 | # for materialSlot in object.material_slots: 2266 | # materialSlot: bpy.types.MaterialSlot 2267 | # # materialSlot.link = 'FastPBRNormal' 2268 | # materialSlot.material = bpy.data.materials.get('FastPBRNormal') 2269 | # materialSlot 2270 | ################################################ 2271 | 2272 | # print("File path:", pathlib.Path(__file__).parent.resolve()) 2273 | # bpy.ops.wm.append( 2274 | # filepath="cube.blend", 2275 | # directory=pathToAssetsFile, 2276 | # filename="Cube") 2277 | # bpy.ops.wm.append( 2278 | # filepath="cube.blend", 2279 | # directory="C:/Program Files/Blender Foundation/blender-3.0.0-alpha+master.2b64b4d90d67-windows.amd64-release/3.0/scripts/addons/fast_pbr_viewport_render/FastPBRAssets.blend\\Object", 2280 | # filename="Cube") 2281 | # return {'FINISHED'} 2282 | 2283 | # print("IMPORTANTEEE" + getGlobalAddonProperties().target_directory) 2284 | # @bookmark _Debugsession 66 2285 | for renderPass in renderPasses: 2286 | if not renderPass == RenderPass: 2287 | renderPass: RenderPass 2288 | 2289 | renderPass.prepare_render_and_save() 2290 | print(str(renderPass) + "Woohoo") # Success 2291 | 2292 | # renderPass. 2293 | # print("Weird?") 2294 | # for key, value in retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property: 2295 | # print(f"HEEEY, key: {key} value: {value}") 2296 | # for index in range(len(retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property)-1): 2297 | # print("Key: " + bpy.types.CollectionProperty().keys()[index]) 2298 | # print("Value: " + bpy.types.CollectionProperty().get[index]) 2299 | 2300 | replacementDictionary = dict() 2301 | 2302 | for item in retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property: 2303 | # print(str(item.value) + "item") 2304 | replacementDictionary[item.key] = item.value 2305 | 2306 | print(f""" 2307 | replacementDictionary: {replacementDictionary} 2308 | pathToStoreImagesIn: {pathToStoreImagesIn} 2309 | retrieveSettings(GlobalSettings).export_file_path: {retrieveSettings(GlobalSettings).export_file_path}""") 2310 | 2311 | moveImages(pathToStoreImagesIn, retrieveSettings(GlobalSettings).export_file_path, replacementDictionary) 2312 | 2313 | # os.system(f'explorer "{targetFolder}"') 2314 | # print(f'FINAL CMD: explorer "{targetFolder}"') 2315 | 2316 | 2317 | # moveImages(pathToStoreImagesIn, getGlobalAddonProperties().target_directory, replacementDictionary) 2318 | 2319 | BackupPrepareAndRestore.restoreSettings() 2320 | 2321 | # bpy.props.CollectionProperty 2322 | # retrieveSettings(GlobalSettings).file_path_replacement_keys_collection_property 2323 | 2324 | 2325 | # @todo Feed path replacement keys to moveImages. Figure out how to extract stuff from the damn collectionProperty. 2326 | 2327 | # 2328 | # for file in os.listdir(pathToStoreImagesIn): 2329 | # print(file) 2330 | 2331 | # test.prepare_render_and_save() 2332 | 2333 | 2334 | ##################### 2335 | # Render normal map # 2336 | ##################### 2337 | 2338 | # bpy.context.scene.render.engine = 'BLENDER_WORKBENCH' 2339 | # bpy.context.scene.display.shading.light = 'MATCAP' 2340 | # bpy.context.scene.display.shading.studio_light = 'check_normal+y.exr' 2341 | # bpy.context.scene.display.shading.color_type = 'SINGLE' 2342 | 2343 | # # Color 2344 | # bpy.context.scene.display.shading.single_color = (1, 1, 1) 2345 | 2346 | 2347 | # # Options 2348 | # bpy.context.scene.display.shading.show_object_outline = False 2349 | # bpy.context.scene.display.shading.use_dof = False 2350 | # bpy.context.scene.display.shading.show_cavity = False 2351 | # bpy.context.scene.display.shading.show_shadows = False 2352 | # bpy.context.scene.display.shading.show_xray = False 2353 | # bpy.context.scene.display.shading.show_backface_culling = False 2354 | 2355 | 2356 | # bpy.context.scene.display.shading.show_backface_culling = False 2357 | 2358 | 2359 | 2360 | 2361 | 2362 | # for area in bpy.context.screen.areas: 2363 | # if area.type == 'VIEW_3D': 2364 | # if bpy.context.active_object.mode == 'EDIT': 2365 | # area.spaces[0].shading.render_pass = 'NORMAL' 2366 | # 2367 | # This loop was supposed to allow the user to select multiple objects and set the modifier collection based on 2368 | # what is set to the first newBoolean, however it seems layout.prop() cant delay the execution in the script 2369 | # hence making this code run before the user have selected a collection. Not sure how to get by this. 2370 | # for object in bpy.context.selected_objects: 2371 | # newBoolean_ = object.modifiers.new(type='BOOLEAN', name='Boolean') 2372 | # newBoolean_.operand_type = 'COLLECTION' 2373 | # newBoolean_.solver = 'FAST' 2374 | # newBoolean_.collection = newBoolean.collection 2375 | 2376 | return {'FINISHED'} # Lets Blender know the operator finished successfully. 2377 | 2378 | # test = "wa" 2379 | # test.rfind() 2380 | 2381 | # class CreateFastCollectionBoolean(bpy.types.Operator): 2382 | # """Fast collection booleans""" # Use this as a tooltip for menu items and buttons. 2383 | # bl_idname = "object.create_fast_collection_boolean" # Unique identifier for buttons and menu items to reference. 2384 | # bl_label = "Create fast collection boolean" # Display name in the interface. 2385 | # bl_options = {'REGISTER', 'UNDO'} # Enable undo for the operator. 2386 | 2387 | # def execute(self, context): # execute() is called when running the operator. 2388 | # # for object in bpy.context.active_object.modifiers: 2389 | # # print("h") 2390 | # # object.name 2391 | # # for property in bpy.context.active_object.modifiers[0].bl_rna.properties: 2392 | # # print(property) 2393 | 2394 | # # print(bpy.context.active_object.modifiers[0].type) 2395 | 2396 | 2397 | # # for object in bpy.context.active_object.modifiers[0].name 2398 | 2399 | 2400 | # collectionBooleansIndex = [] 2401 | # currentIndex = -1 2402 | # modifier: bpy.types.Modifier 2403 | # for modifier in bpy.context.active_object.modifiers: 2404 | # currentIndex += 1 2405 | # if modifier.type == "BOOLEAN": 2406 | # print("EQUALS BOOLEAN") 2407 | # else: 2408 | # print("Not BOOLEAN") 2409 | # print(modifier.type) 2410 | # if modifier.operand_type == "COLLECTION": 2411 | # print("COLLECTION OPERAND TYPE") 2412 | # else: 2413 | # print("Not COLLECTION OPERAND TYPE") 2414 | 2415 | 2416 | # if modifier.type == "BOOLEAN" and modifier.operand_type == "COLLECTION": 2417 | # collectionBooleansIndex.append(currentIndex) 2418 | 2419 | 2420 | 2421 | 2422 | # #os.system("cls") 2423 | # # The original script 2424 | # newBoolean = bpy.context.active_object.modifiers.new(type='BOOLEAN', name='Boolean') 2425 | # newBoolean.operand_type = 'COLLECTION' 2426 | # newBoolean.solver = 'FAST' 2427 | # def draw_func(self, context): 2428 | # self.layout.prop(newBoolean, "collection", text="") 2429 | # bpy.context.window_manager.popup_menu(draw_func, title="Select cutter collection", icon='MOD_BOOLEAN') 2430 | # # 2431 | # # This loop was supposed to allow the user to select multiple objects and set the modifier collection based on 2432 | # # what is set to the first newBoolean, however it seems layout.prop() cant delay the execution in the script 2433 | # # hence making this code run before the user have selected a collection. Not sure how to get by this. 2434 | # # for object in bpy.context.selected_objects: 2435 | # # newBoolean_ = object.modifiers.new(type='BOOLEAN', name='Boolean') 2436 | # # newBoolean_.operand_type = 'COLLECTION' 2437 | # # newBoolean_.solver = 'FAST' 2438 | # # newBoolean_.collection = newBoolean.collection 2439 | 2440 | # return {'FINISHED'} # Lets Blender know the operator finished successfully. 2441 | 2442 | # @bookmark property group 2443 | # @registerClassToBPYDecorator 2444 | # @registerOperatorsDecorator 2445 | 2446 | # @registerClassToBPYDecorator 2447 | # class PropertyGrouBaseClass(bpy.types.PropertyGroup,object): 2448 | # __metaclass__ = registerClassToBPYDecorator 2449 | 2450 | 2451 | class PerSceneAddonProperties(bpy.types.PropertyGroup): 2452 | file_path: bpy.props.StringProperty(name="File path", 2453 | description="Some elaborate description", 2454 | default="", 2455 | maxlen=1024, 2456 | subtype="FILE_PATH") 2457 | 2458 | # @registerOperatorsDecorator # The decorator appears unable to register PropertyGroups for some reason!! 2459 | class FastPBR(PerSceneAddonProperties): 2460 | pass 2461 | classesToRegister.append(FastPBR) 2462 | 2463 | def getNameOfVariable(variable): 2464 | return f'{variable=}'.split('=')[0] 2465 | def getNameOfVariableAndReplaceUnderscoreWithSpace(variable): 2466 | return getNameOfVariable(variable).replace("_", " ") 2467 | 2468 | class GlobalAddonProperties(): 2469 | # target_directory: str = "" 2470 | target_directory: bpy.props.StringProperty("Target Directory", 2471 | description="Some elaborate description", 2472 | default="", 2473 | maxlen=2048, 2474 | subtype="FILE_PATH") 2475 | 2476 | 2477 | 2478 | class AddonPreferencesUI(): 2479 | def draw(self, context): 2480 | self.layout.label(text=f"Welcome to {addonName}!") 2481 | 2482 | def getGlobalAddonProperties(): 2483 | return bpy.context.preferences.addons[__name__].preferences 2484 | 2485 | class FastPBRPreferences(bpy.types.AddonPreferences, GlobalAddonProperties, AddonPreferencesUI): 2486 | bl_idname = __name__ 2487 | 2488 | classesToRegister.append(FastPBRPreferences) 2489 | 2490 | # @bookmark register 2491 | 2492 | 2493 | # @TODO automatic system for menu registration 2494 | # # This loop generates addmenu callback functions necessary for BPY to register a menu to an existing menu or header. 2495 | # # These functions are later fed inside register() 2496 | # for menuClass in menusToRegister: 2497 | # exec(f'''def {retrieveOperatorFromCls(menuClass).replace('.', '_')}_addmenu(self, context): 2498 | # self.layout.menu({retrieveOperatorFromCls(menuClass)})''') 2499 | 2500 | 2501 | 2502 | 2503 | def register(): 2504 | # bpy.utils.register_class(FastPBRViewportRender) 2505 | ############################# 2506 | ###### Install MatCaps ###### 2507 | ############################# 2508 | shouldInstallMatcaps = True 2509 | if(shouldInstallMatcaps): 2510 | normalMatCapFound = False 2511 | for studiolight in bpy.context.preferences.studio_lights: 2512 | studiolight: bpy.types.StudioLight 2513 | print(studiolight.name) 2514 | if(studiolight.name.__contains__("FastPBRNormalMatCap")): 2515 | normalMatCapFound = True 2516 | break 2517 | 2518 | if(normalMatCapFound): 2519 | print("The normal matcap was found") 2520 | else: 2521 | print("No normal matcap doesnt appear to be install, attempting to install it") 2522 | pathToNormalMatcapExr = pathToAddonDirectory + "/FastPBRNormalMatCap.exr" 2523 | if(os.path.exists(pathToNormalMatcapExr)): 2524 | shutil.copy2(pathToNormalMatcapExr,pathToBlenderExeDirectoryExcludingTheExeFile + "/3.0/datafiles/studiolights/matcap") 2525 | bpy.context.preferences.studio_lights.refresh() 2526 | else: 2527 | warningMessage = f'We cant seem to find the Exr file for the normal matcap and therefore cant install it, please check that you have the file ->"{pathToNormalMatcapExr}"<-' 2528 | print(warningMessage) 2529 | ShowPopupMessageBoxAtCursor(warningMessage) 2530 | #################################### 2531 | ###### End of Install MatCaps ###### 2532 | #################################### 2533 | 2534 | os.system("cls") 2535 | print(putTextInBox(f"Registering {addonName}")) 2536 | # print("Globals:" + str(globals())) 2537 | 2538 | # print(__name__) 2539 | alreadyRegistered = list() 2540 | 2541 | for cls in classesToRegister: 2542 | if not cls in alreadyRegistered: 2543 | alreadyRegistered.append(cls) 2544 | print("Registering operator:", str(cls)) 2545 | bpy.utils.register_class(cls) 2546 | 2547 | # bpy.context.scene.fast_pbr.render_pass.file_path = RenderNormalPassWithWorkbench.settings.file_path 2548 | 2549 | 2550 | # bpy.types.Scene.fast_pbr=bpy.types.PointerProperty(type=(RenderNormalPassWithWorkbench.settings)) 2551 | 2552 | # bpy.types.Scene.fast_pbr = bpy.props.PointerProperty(type=FastPBR) 2553 | 2554 | for settingsPropertyGroupParent in settingsPropertyGroupParents: 2555 | 2556 | print(f'''Adding settingsPropertyGroup: "{settingsPropertyGroupParent.__name__}" to the scene as "bpy.types.Scene.{PascalCaseTo_snake_case(addonNameShort + '_' + settingsPropertyGroupParent.__name__)}"''') 2557 | # renderPass: RenderPass = RenderPass 2558 | 2559 | renderPassSettingsVariables = list() 2560 | # for variableName in renderPass.settings.__dict__: 2561 | # for variableName in dir(renderPass.settings): 2562 | # for variableName in renderPass.settings.items(renderPass.settings): 2563 | # for variableName in [attr for attr in dir(renderPass.settings) if not callable(getattr(renderPass.settings, attr)) and not attr.startswith("__")]: 2564 | for variableName in settingsPropertyGroupParent.settings.__annotations__.keys(): 2565 | print("variableName: " + variableName) 2566 | if not variableName[0] == '_': 2567 | renderPassSettingsVariables.append(variableName) 2568 | 2569 | for variableName in renderPassSettingsVariables: 2570 | # exec(f"bpy.context.scene.{PascalCaseTo_snake_case(addonNameShort + '.' + renderPass.__name__)}.{variableName} = {renderPass.__name__}.settings.{variableName}") 2571 | # print(f"bpy.types.Scene.{PascalCaseTo_snake_case(addonNameShort + '_' + renderPass.__name__)} = {renderPass.__name__}.settings.{variableName}") 2572 | # exec(f"bpy.types.Scene.{PascalCaseTo_snake_case(addonNameShort + '_' + renderPass.__name__)} = {renderPass.__name__}.settings.{variableName}") 2573 | 2574 | 2575 | 2576 | print(f"bpy.types.Scene.{PascalCaseTo_snake_case(addonNameShort + '_' + settingsPropertyGroupParent.__name__)} = bpy.props.PointerProperty(type={settingsPropertyGroupParent.__name__}.settings)") 2577 | exec(f"bpy.types.Scene.{PascalCaseTo_snake_case(addonNameShort + '_' + settingsPropertyGroupParent.__name__)} = bpy.props.PointerProperty(type={settingsPropertyGroupParent.__name__}.settings)") 2578 | # bpy.types.Scene.fast_pbr_render_normal_pass_with_workbench = bpy.props.PointerProperty(type=FastPBR) 2579 | 2580 | 2581 | 2582 | # exec(f"{renderPass.__name__}.retrieveSettings().{variableName} = {renderPass.__name__}.settings.{variableName}") 2583 | 2584 | # exec(f"""print("in my ass{renderPass.__name__}")""") 2585 | # Emulates something like: 2586 | # bpy.context.scene.fast_pbr.render_pass.file_path = renderPass.settings.file_path 2587 | 2588 | 2589 | 2590 | # bpy.utils.register_tool(MyTool, after={"builtin.scale_cage"}, separator=True, group=True) 2591 | # bpy.utils.register_tool(MyOtherTool, after={MyTool.bl_idname}) 2592 | 2593 | 2594 | # Instead of: 2595 | # bpy.types.Scene.fast_pbr = bpy.props.PointerProperty(type=FastPBR) 2596 | # Is there something like: 2597 | # createPropertyGroupObject("bpy.types.Scene.fast_pbr", bpy.props.PointerProperty(type=FastPBR)) # Psuedo code for doing the same thing. 2598 | # Basically I want to be able to put it in a loop iterating over a list of classes. 2599 | 2600 | # @TODO automatic system for menu registration ###### Start 2601 | # # Append menus to existing menus in the order that there cooresponding classes are declared in the addon. 2602 | # print(f"menusToRegister: {menusToRegister}") 2603 | # for menuClass in menusToRegister: 2604 | # print(f"menuClass: {menusToRegister}") 2605 | # print(f"menuClass: {menusToRegister}") 2606 | # for menuToRegisterMenuClassUnder in menusToRegister[menuClass]: 2607 | # print(f"menusToRegister[menuClass]: {menusToRegister[menuClass]}") 2608 | # print(f"menuToRegisterMenuClassUnder: {menuToRegisterMenuClassUnder}") 2609 | # menuToRegisterMenuClassUnder: str() 2610 | # exec(f"bpy.types.{menuToRegisterMenuClassUnder}.append({retrieveOperatorFromCls(menuClass).replace('.', '_')}_addmenu)") 2611 | # # bpy.types.VIEW3D_MT_editor_menus.append(addmenu_callback) 2612 | ########### End 2613 | 2614 | 2615 | 2616 | print(putTextInBox(f"Registration complete")) 2617 | 2618 | 2619 | 2620 | 2621 | 2622 | def unregister(): 2623 | # bpy.utils.unregister_class(FastPBRViewportRender) 2624 | 2625 | print(putTextInBox(f"Unregistering {addonName}")) 2626 | 2627 | 2628 | alreadyRegistered = list() 2629 | 2630 | for cls in classesToRegister: 2631 | if not cls in alreadyRegistered: 2632 | alreadyRegistered.append(cls) 2633 | print("Unregistering operator:", str(cls)) 2634 | bpy.utils.unregister_class(cls) 2635 | 2636 | # for cls in classesToRegister: 2637 | # print("Unregistering operator:", str(cls)) 2638 | # bpy.utils.unregister_class(cls) 2639 | 2640 | # bpy.utils.unregister_tool(MyTool) 2641 | # bpy.utils.unregister_tool(MyOtherTool) 2642 | 2643 | # bpy.utils.unregister_class(FastPBR) 2644 | # del bpy.types.Scene.fast_pbr 2645 | 2646 | 2647 | # for settingsPropertyGroupParent in settingsPropertyGroupParents: 2648 | 2649 | # print(f'''Removing settingsPropertyGroup: "{settingsPropertyGroupParent.__name__}" from the scene as "bpy.types.Scene.{PascalCaseTo_snake_case(addonNameShort + '_' + settingsPropertyGroupParent.__name__)}"''') 2650 | 2651 | # renderPassSettingsVariables = list() 2652 | # for variableName in settingsPropertyGroupParent.settings.__annotations__.keys(): 2653 | # print("variableName: " + variableName) 2654 | # if not variableName[0] == '_': 2655 | # renderPassSettingsVariables.append(variableName) 2656 | 2657 | # for variableName in renderPassSettingsVariables: 2658 | # toExec = f"del bpy.types.Scene.{PascalCaseTo_snake_case(addonNameShort + '_' + settingsPropertyGroupParent.__name__)}" 2659 | # print("EXEC -> " + toExec) 2660 | # exec(toExec) 2661 | 2662 | 2663 | 2664 | print(putTextInBox(f"Unregistration complete")) 2665 | 2666 | 2667 | 2668 | 2669 | 2670 | 2671 | 2672 | # This allows you to run the script directly from Blender's Text editor 2673 | # to test the add-on without having to install it. 2674 | if __name__ == "__main__": 2675 | register() -------------------------------------------------------------------------------- /fast_pbr_viewport_render/fileRenamer.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import sys 3 | 4 | 5 | 6 | import shutil 7 | import os 8 | import glob 9 | print() 10 | 11 | from PIL import Image 12 | 13 | import time 14 | 15 | # shutil. 16 | def is_locked(filepath): 17 | locked = None 18 | file_object = None 19 | if os.path.exists(filepath): 20 | try: 21 | buffer_size = 8 22 | # Opening file in append mode and read the first 8 characters. 23 | file_object = open(filepath, 'a', buffer_size) 24 | if file_object: 25 | locked = False 26 | except IOError as message: 27 | locked = True 28 | finally: 29 | if file_object: 30 | file_object.close() 31 | return locked 32 | 33 | import errno, os 34 | 35 | # Sadly, Python fails to provide the following magic number for us. 36 | ERROR_INVALID_NAME = 123 37 | 38 | def is_pathname_valid(pathname: str) -> bool: 39 | ''' 40 | `True` if the passed pathname is a valid pathname for the current OS; 41 | `False` otherwise. 42 | ''' 43 | # If this pathname is either not a string or is but is empty, this pathname 44 | # is invalid. 45 | try: 46 | if not isinstance(pathname, str) or not pathname: 47 | return False 48 | 49 | # Strip this pathname's Windows-specific drive specifier (e.g., `C:\`) 50 | # if any. Since Windows prohibits path components from containing `:` 51 | # characters, failing to strip this `:`-suffixed prefix would 52 | # erroneously invalidate all valid absolute Windows pathnames. 53 | _, pathname = os.path.splitdrive(pathname) 54 | 55 | # Directory guaranteed to exist. If the current OS is Windows, this is 56 | # the drive to which Windows was installed (e.g., the "%HOMEDRIVE%" 57 | # environment variable); else, the typical root directory. 58 | root_dirname = os.environ.get('HOMEDRIVE', 'C:') \ 59 | if os.sys.platform == 'win32' else os.path.sep 60 | assert os.path.isdir(root_dirname) # ...Murphy and her ironclad Law 61 | 62 | # Append a path separator to this directory if needed. 63 | root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep 64 | 65 | # Test whether each path component split from this pathname is valid or 66 | # not, ignoring non-existent and non-readable path components. 67 | for pathname_part in pathname.split(os.path.sep): 68 | try: 69 | os.lstat(root_dirname + pathname_part) 70 | # If an OS-specific exception is raised, its error code 71 | # indicates whether this pathname is valid or not. Unless this 72 | # is the case, this exception implies an ignorable kernel or 73 | # filesystem complaint (e.g., path not found or inaccessible). 74 | # 75 | # Only the following exceptions indicate invalid pathnames: 76 | # 77 | # * Instances of the Windows-specific "WindowsError" class 78 | # defining the "winerror" attribute whose value is 79 | # "ERROR_INVALID_NAME". Under Windows, "winerror" is more 80 | # fine-grained and hence useful than the generic "errno" 81 | # attribute. When a too-long pathname is passed, for example, 82 | # "errno" is "ENOENT" (i.e., no such file or directory) rather 83 | # than "ENAMETOOLONG" (i.e., file name too long). 84 | # * Instances of the cross-platform "OSError" class defining the 85 | # generic "errno" attribute whose value is either: 86 | # * Under most POSIX-compatible OSes, "ENAMETOOLONG". 87 | # * Under some edge-case OSes (e.g., SunOS, *BSD), "ERANGE". 88 | except OSError as exc: 89 | if hasattr(exc, 'winerror'): 90 | if exc.winerror == ERROR_INVALID_NAME: 91 | return False 92 | elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}: 93 | return False 94 | # If a "TypeError" exception was raised, it almost certainly has the 95 | # error message "embedded NUL character" indicating an invalid pathname. 96 | except TypeError as exc: 97 | return False 98 | # If no exception was raised, all path components and hence this 99 | # pathname itself are valid. (Praise be to the curmudgeonly python.) 100 | else: 101 | return True 102 | # If any other exception was raised, this is an unrelated fatal issue 103 | # (e.g., a bug). Permit this exception to unwind the call stack. 104 | # 105 | # Did we mention this should be shipped with Python already? 106 | 107 | def wait_for_file(filepath): 108 | wait_time = 1 109 | while is_locked(filepath): 110 | print("File lock detected!") 111 | time.sleep(wait_time) 112 | import bpy 113 | def moveImages(sourceDirectory, targetDirectory, replacementDictionary, removeOriginalFiles = True): 114 | # sourceDirectory = "C:\FastPBRViewportRender" 115 | 116 | fullPathToDesktop = os.path.join(os.path.join(os.path.expanduser('~')), 'Desktop') 117 | fullPathToUserHomePath = os.path.join(os.path.expanduser('~')) 118 | 119 | 120 | replacementDictionary["fullPathToDesktop"] = fullPathToDesktop 121 | replacementDictionary["fullPathToUserHomePath"] = fullPathToUserHomePath 122 | 123 | 124 | 125 | 126 | 127 | if isinstance(replacementDictionary, bpy.types.CollectionProperty): 128 | print("ReplacementDictionary is of type CollectionProperty") 129 | 130 | replacementDictionaryTemp = dict() 131 | for item in replacementDictionary: 132 | # replacementDictionaryTemp[str(key)] = str(replacementDictionary[key]) 133 | replacementDictionaryTemp[item.key] = item.value 134 | replacementDictionary = replacementDictionaryTemp 135 | print(replacementDictionary) 136 | 137 | 138 | sourceDirectory = sourceDirectory.replace('\\', '/') 139 | if not sourceDirectory[-1] == '/': 140 | sourceDirectory = sourceDirectory + '/' 141 | 142 | 143 | # replacementDictionary = {"name": "myFancyName", "var1Key": "var1Value"} 144 | 145 | 146 | # targetDirectory = r"C:/testTarget/{name}/FromFastPBR/{name}{imageSizeShort}_{fileNameWithExtension}" 147 | targetDirectory = targetDirectory.replace('\\', '/') 148 | sourceDirectory = sourceDirectory.replace('\\', '/') 149 | # if not targetDirectory[-1] == '/': 150 | # targetDirectory = targetDirectory + '/' 151 | 152 | from os import walk 153 | 154 | # f = [] 155 | # for (dirpath, dirnames, filenames) in walk(directory): 156 | # print(f"dirpath: {dirpath} dirnames: {dirnames} filenames: {filenames} ") 157 | 158 | 159 | 160 | # f = [] 161 | 162 | # walkOut = walk(directory) 163 | # for i in range(5): 164 | # # print(walk(directory)[0]) 165 | # print(walkOut[0]) 166 | # # print(f"dirpath: {dirpath} dirnames: {dirnames} filenames: {filenames} ") 167 | 168 | 169 | sourceDirectory 170 | 171 | for replaceFromWithoutBrackets in replacementDictionary: 172 | replaceFrom = '{' + replaceFromWithoutBrackets + '}' 173 | replaceTo = replacementDictionary[replaceFromWithoutBrackets] 174 | while replaceFrom in sourceDirectory: 175 | sourceDirectory = sourceDirectory.replace(replaceFrom, str(replaceTo)) 176 | 177 | if not os.path.exists(sourceDirectory): 178 | os.makedirs(sourceDirectory) 179 | 180 | # if sourceDirectory[-1] == '/': 181 | # sourceDirectory = sourceDirectory [:-1] 182 | 183 | files = os.listdir(sourceDirectory) 184 | didWeFindAnyFilesInSourceDir = False 185 | print(f"files: {files}") 186 | for fileNameWithExtension in files: 187 | didWeFindAnyFilesInSourceDir = True 188 | print("fileNameWithExtension: " + fileNameWithExtension) 189 | 190 | fileNameWithoutExtension = fileNameWithExtension.split('.')[0] 191 | fileExtensionWithoutDot = fileNameWithExtension.split('.')[1] 192 | fileWithPathAndExtension = sourceDirectory + fileNameWithExtension 193 | wait_for_file(fileWithPathAndExtension) 194 | try: 195 | im = Image.open(fileWithPathAndExtension) 196 | except IOError as ErrorMessage: 197 | print(f""" 198 | fastPBR: We believe we just detected non-image in the directory that we're moving the images from 199 | Skipping this file (or possibly directory) and moving on to the next: 200 | {fileWithPathAndExtension} 201 | Error message by PIL: 202 | {ErrorMessage} 203 | 204 | Otherwise the images might be broken. PIL cannot open them non the less.""") 205 | continue 206 | 207 | imageWidth, imageHeight = im.size 208 | imageSizeInPixels = 0 209 | if imageWidth > imageHeight: 210 | imageSizeInPixels = imageWidth 211 | else: 212 | imageSizeInPixels = imageHeight 213 | im.close() 214 | 215 | imageSizeShort = '' 216 | if imageSizeInPixels == 65536: # No, this is not excessive at all 217 | imageSizeShort = '64K' 218 | if imageSizeInPixels == 32768: 219 | imageSizeShort = '32K' 220 | if imageSizeInPixels == 16384: 221 | imageSizeShort = '16K' 222 | if imageSizeInPixels == 8192: 223 | imageSizeShort = '8K' 224 | if imageSizeInPixels == 4096: 225 | imageSizeShort = '4K' 226 | elif imageSizeInPixels == 2048: 227 | imageSizeShort = '2K' 228 | elif imageSizeInPixels == 1024: 229 | imageSizeShort = '1K' 230 | else: 231 | imageSizeShort = str(imageSizeInPixels) + 'PX' 232 | 233 | targetDirectory: str 234 | sourceDirectory: str 235 | 236 | if sourceDirectory[-1] == '/': 237 | sourceDirectoryWithoutEndingSlash = sourceDirectory[:-1] 238 | else: 239 | sourceDirectoryWithoutEndingSlash = sourceDirectory 240 | 241 | sourceDirectoryTopLevelFolderName = sourceDirectoryWithoutEndingSlash[sourceDirectoryWithoutEndingSlash.rfind('/'):] 242 | 243 | 244 | replacementDictionary["imageSizeShort"] = imageSizeShort 245 | replacementDictionary["imageWidth"] = imageWidth 246 | replacementDictionary["imageHeight"] = imageHeight 247 | replacementDictionary["imageSizeInPixels"] = imageSizeInPixels 248 | replacementDictionary["fileNameWithExtension"] = fileNameWithExtension 249 | replacementDictionary["fileNameWithoutExtension"] = fileNameWithoutExtension 250 | replacementDictionary["fileExtensionWithoutDot"] = fileExtensionWithoutDot 251 | replacementDictionary["fileWithPathAndExtension"] = fileWithPathAndExtension 252 | replacementDictionary["sourceDirectoryTopLevelFolderName"] = sourceDirectoryTopLevelFolderName 253 | replacementDictionary["sourceDirectory"] = sourceDirectory 254 | 255 | 256 | 257 | 258 | 259 | preparedTargetDirectoryAndFileNameWithExtension = targetDirectory 260 | # preparedTargetDirectoryAndFileNameWithExtension = targetDirectory + '/' + 261 | 262 | for replaceFromWithoutBrackets in replacementDictionary: 263 | replaceFrom = '{' + replaceFromWithoutBrackets + '}' 264 | replaceTo = replacementDictionary[replaceFromWithoutBrackets] 265 | while replaceFrom in preparedTargetDirectoryAndFileNameWithExtension: 266 | preparedTargetDirectoryAndFileNameWithExtension = preparedTargetDirectoryAndFileNameWithExtension.replace(replaceFrom, str(replaceTo)) 267 | # print(targetDirectory, "--", replaceFrom, "--", replaceTo) 268 | if not preparedTargetDirectoryAndFileNameWithExtension.__contains__('.'): 269 | ShowPopupMessageBoxAtCursor(f"The target directory {targetDirectory} must include the file name and its extension!", "Fast PBR", 'ERROR') 270 | return {'FINISHED'} 271 | 272 | 273 | print(f"""{fileNameWithExtension}: 274 | fileNameWithoutExtension: {fileNameWithoutExtension} 275 | fileExtension: {fileExtensionWithoutDot} 276 | fileWithPathAndExtension: {fileWithPathAndExtension} 277 | preparedTargetDirectory: {preparedTargetDirectoryAndFileNameWithExtension} 278 | sourceDirectoryTopLevelFolderName: {sourceDirectoryTopLevelFolderName} 279 | sourceDirectory: {sourceDirectory}""") 280 | # print() 281 | preparedTargetDirectoryAndFileNameWithExtension: str 282 | # preparedTargetDirectoryWithoutFile = '/'.join(preparedTargetDirectoryAndFileNameWithExtension.split('/')[0:-1]) 283 | preparedTargetDirectoryWithoutFile = preparedTargetDirectoryAndFileNameWithExtension[:preparedTargetDirectoryAndFileNameWithExtension.rfind('/')] 284 | # if not is_pathname_valid(preparedTargetDirectoryAndFileNameWithExtension): 285 | # print("The destination path appears to be invalid:\n " + str(preparedTargetDirectoryAndFileNameWithExtension)) 286 | # return 287 | if not os.path.exists(preparedTargetDirectoryWithoutFile): 288 | os.makedirs(preparedTargetDirectoryWithoutFile) 289 | shutil.copy2(fileWithPathAndExtension, preparedTargetDirectoryAndFileNameWithExtension) 290 | 291 | 292 | if removeOriginalFiles: 293 | os.remove(fileWithPathAndExtension) 294 | if not didWeFindAnyFilesInSourceDir: 295 | ErrorMsg = f'''There are no images in your source path! The source path we tried reading was: "{sourceDirectory}"''' 296 | ShowPopupMessageBoxAtCursor(ErrorMsg, "Error", 'ERROR') 297 | return {'FINISHED'} 298 | else: 299 | ShowPopupMessageBoxAtCursor("Your images are now in the destination folder!", "Fast PBR", 'INFO') 300 | 301 | pathForExplorer = preparedTargetDirectoryWithoutFile.replace('/', '\\') 302 | while pathForExplorer.find('\\\\') > -1: 303 | pathForExplorer = pathForExplorer.replace('\\\\', '\\') 304 | os.system(f'explorer "{pathForExplorer}"') 305 | print(f'Opening explorer -> explorer "{pathForExplorer}"') 306 | 307 | return {'FINISHED'} 308 | # os.rename(r'C:\Users\Administrator.SHAREPOINTSKY\Desktop\Work\name.txt',r'C:\Users\Administrator.SHAREPOINTSKY\Desktop\Newfolder\details.txt' ) 309 | 310 | def ShowPopupMessageBoxAtCursor(message = "", title = "Message Box", icon = 'INFO'): 311 | 312 | def draw(self, context): 313 | self.layout.label(text=message) 314 | 315 | print(message) 316 | return {'FINISHED'} 317 | 318 | bpy.context.window_manager.popup_menu(draw, title = title, icon = icon) 319 | 320 | # moveImages("C:\FastPBRViewportRender", r"C:/testTarget/{name}/FromFastPBR/{name}{imageSizeShort}_{fileNameWithExtension}") 321 | 322 | 323 | 324 | 325 | # break 326 | # for file in directory 327 | # shutil.move(file, '/home/user/Documents/useful_name.txt') -------------------------------------------------------------------------------- /sword.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ItsCubeTime/FastPBR/2f824a62b48d9a805e4c2589876138818369a81c/sword.mp4 --------------------------------------------------------------------------------