├── Editor ├── Assets │ ├── BatchImportAssetPackagesWindow.cs.meta │ ├── Cleanup.meta │ ├── FindReference.cs.meta │ ├── TextureCombiner.cs.meta │ ├── BatchExtractMaterials.cs.meta │ ├── Cleanup │ │ ├── RemoveEmptyFolders.cs.meta │ │ ├── UnityGuidRegenerator.cs.meta │ │ ├── FindMissingScriptsRecursivelyAndRemove.cs.meta │ │ ├── FindMissingScriptsRecursivelyAndRemove.cs │ │ ├── RemoveEmptyFolders.cs │ │ └── UnityGuidRegenerator.cs │ ├── FindReference.cs │ ├── BatchImportAssetPackagesWindow.cs │ ├── TextureCombiner.cs │ └── BatchExtractMaterials.cs ├── Assets.meta ├── Edit.meta ├── Window.meta ├── Edit │ ├── NormalMapFixer │ │ ├── README.md.meta │ │ ├── NormalFixer.cs.meta │ │ ├── README.md │ │ └── NormalFixer.cs │ ├── NormalMapFixer.meta │ ├── Shortcuts.cs.meta │ ├── LockToggle.cs.meta │ ├── PhysicsSettler.cs.meta │ ├── CreateEmptyAtRoot.cs.meta │ ├── DistributeEvenly.cs.meta │ ├── SortSceneHierarchy.cs.meta │ ├── TextToTextMeshPro.cs.meta │ ├── ReplaceSelectionWindow.cs.meta │ ├── ResetParentTransform.cs.meta │ ├── ReplaceMaterialInChildren.cs.meta │ ├── GlobalDefinesWizard.cs.meta │ ├── CreateEmptyAtRoot.cs │ ├── ReplaceMaterialInChildren.cs │ ├── ResetParentTransform.cs │ ├── SortSceneHierarchy.cs │ ├── DistributeEvenly.cs │ ├── PhysicsSettler.cs │ ├── LockToggle.cs │ ├── Shortcuts.cs │ ├── ReplaceSelectionWindow.cs │ ├── GlobalDefinesWizard.cs │ └── TextToTextMeshPro.cs ├── Window │ ├── Analysis.meta │ ├── Sequencing.meta │ ├── Analysis │ │ ├── ResourceChecker.cs.meta │ │ ├── ConsoleCallStackHelper.cs.meta │ │ └── ConsoleCallStackHelper.cs │ └── Sequencing │ │ ├── DuplicateTimeline.cs.meta │ │ └── DuplicateTimeline.cs ├── PixelWizards.Utilities.asmdef.meta └── PixelWizards.Utilities.asmdef ├── Documentation~ ├── Edit │ ├── LockToggle.md │ ├── PhysicsSettler.md │ ├── GlobalDefinesWizard.md │ ├── ReplaceMaterialinChildren.md │ ├── CreateEmptyatRoot.md │ ├── DistributeEvenly.md │ ├── index.md │ ├── SortSceneHierarchy.md │ ├── ReplaceSelection.md │ ├── TexttoTextMeshPro.md │ └── AssortedShortcuts.md ├── Assets │ ├── FindReference.md │ ├── SimpleTextureCombiner.md │ ├── Cleanup.md │ ├── index.md │ ├── Cleanup │ │ ├── UnityGUIDRegenerator.md │ │ ├── RemoveEmptyFolders.md │ │ └── FindMissingScripts.md │ ├── BatchImportUnityPackage.md │ └── BatchExtractMaterials.md ├── Window │ ├── Analysis │ │ ├── ConsoleCallstackHelper.md │ │ └── ResourceChecker.md │ ├── Sequencer │ │ └── DuplicateTimeline.md │ └── index.md ├── Images │ ├── BatchExtractWindow.png │ ├── BatchImportPackages.png │ ├── FindMissingScripts.png │ ├── GUIDRegeneration.png │ ├── ReplaceSelection.png │ ├── ResourceChecker.png │ ├── materialmultiedit.png │ ├── RemoveEmptyFoldersChecked.png │ ├── RemoveEmptyFoldersDefault.png │ ├── ReplaceSelectionButton.png │ ├── VerifyExtractMaterials.png │ └── MultipleObjectsBatchMaterial.png └── index.md ├── CHANGELOG.md.meta ├── LICENSE.md.meta ├── README.md.meta ├── CONTRIBUTING.md.meta ├── package.json.meta ├── CODE_OF_CONDUCT.md.meta ├── Editor.meta ├── README.md ├── CHANGELOG.md ├── package.json ├── CONTRIBUTING.md ├── LICENSE.md ├── .gitignore ├── .gitattributes └── CODE_OF_CONDUCT.md /Editor/Assets/BatchImportAssetPackagesWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: db5d0ffaa9034009a9f4ae64efbeea63 3 | timeCreated: 1750643970 -------------------------------------------------------------------------------- /Documentation~/Edit/LockToggle.md: -------------------------------------------------------------------------------- 1 | # Lock Toggle 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Assets/FindReference.md: -------------------------------------------------------------------------------- 1 | # Find Reference 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Edit/PhysicsSettler.md: -------------------------------------------------------------------------------- 1 | # Physics Settler 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Edit/GlobalDefinesWizard.md: -------------------------------------------------------------------------------- 1 | # Global Defines Wizard 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Assets/SimpleTextureCombiner.md: -------------------------------------------------------------------------------- 1 | # Simple Texture Combiner 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Edit/ReplaceMaterialinChildren.md: -------------------------------------------------------------------------------- 1 | # Replace Material in Children 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Window/Analysis/ConsoleCallstackHelper.md: -------------------------------------------------------------------------------- 1 | # Console Callstack Helper 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Images/BatchExtractWindow.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:da6ba2e3974666f7a87c1bd6940aa884bb32e3b67d5652e8c0a6fa5cc860a9e1 3 | size 33144 4 | -------------------------------------------------------------------------------- /Documentation~/Images/BatchImportPackages.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f18674051357f1aa86409be218d729dc39e56af07a8596295851e376b8dc330c 3 | size 21529 4 | -------------------------------------------------------------------------------- /Documentation~/Images/FindMissingScripts.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:cf0a37bcf0488be3fd1c98bf1b2b124460c1a31fd91c8ca69e15dfe089c44247 3 | size 13684 4 | -------------------------------------------------------------------------------- /Documentation~/Images/GUIDRegeneration.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:00d96e531441c2f46c7322428055e8387d5b88a690e036892f80be6e037d1a5d 3 | size 11894 4 | -------------------------------------------------------------------------------- /Documentation~/Images/ReplaceSelection.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:e1e4b6e5c1dc36a656a877d77fc77dcb2c6a6c099e38fc6ea4ea5c12d8b597ce 3 | size 41303 4 | -------------------------------------------------------------------------------- /Documentation~/Images/ResourceChecker.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:d00d78882e0efbced1d677f146ec3d2851e3ae35514814b926036a5c03bc7979 3 | size 251544 4 | -------------------------------------------------------------------------------- /Documentation~/Images/materialmultiedit.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:30f488b638a691a060376a5f0a4af39d39244dd05a1742c845b982f92661284f 3 | size 19458 4 | -------------------------------------------------------------------------------- /Documentation~/Window/Sequencer/DuplicateTimeline.md: -------------------------------------------------------------------------------- 1 | # Duplicate Timeline (with Bindings) 2 | 3 | ## What it does 4 | 5 | 6 | 7 | ## How do you use this 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Documentation~/Images/RemoveEmptyFoldersChecked.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:acc16dd8084ab30bfe200c0d83e760b6dbcc79d38188857b53b00ef9cf09ca9d 3 | size 11443 4 | -------------------------------------------------------------------------------- /Documentation~/Images/RemoveEmptyFoldersDefault.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:4037f2cb8e32212a393e740a8906a3814aef386e995ae593d3823b92f760277c 3 | size 11533 4 | -------------------------------------------------------------------------------- /Documentation~/Images/ReplaceSelectionButton.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:f82ced9baa82d2e686720482c47f022c47e6c665644a9c8571644f57f3e8fbd0 3 | size 40333 4 | -------------------------------------------------------------------------------- /Documentation~/Images/VerifyExtractMaterials.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:3515daa9366a888f8131e40af692a6ea36d01a986ca8801f29bd14a8669f1dd9 3 | size 244062 4 | -------------------------------------------------------------------------------- /Documentation~/Images/MultipleObjectsBatchMaterial.png: -------------------------------------------------------------------------------- 1 | version https://git-lfs.github.com/spec/v1 2 | oid sha256:bd5c4d6ccb6820017280dffc7272c929b12aea59fe2b612a1424045e4e904215 3 | size 41268 4 | -------------------------------------------------------------------------------- /CHANGELOG.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35b1c1c0cc400cb43a5b7981ee6f2b13 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /LICENSE.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3b9de767630bf89479b1569fd23740ff 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 82f27aaef6546b84399ee485fc5bc997 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /CONTRIBUTING.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 334848d72c9f9e540b632cc16c55ace4 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /package.json.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 11affb3cf0f26a243a4edb565dc72278 3 | PackageManifestImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 4a120dbd1e76dea40983966b5e0f2ecd 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6598e2fa933860a4f9b7f536b8aa0090 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Assets.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: e20e16799c931954ab491d1bdb652cc0 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Edit.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 20442533efbed1c49b5d84839ff92769 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Window.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3dec3a6efb30af244883ba390f05486e 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Edit/NormalMapFixer/README.md.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 6e1587cbef4d16c468099b1eb11ba358 3 | TextScriptImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Assets/Cleanup.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 45265ed18a661c546a5017d4f77bb64c 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Window/Analysis.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 25861767bf2a9504585b0335be8ff1ac 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Window/Sequencing.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 3f4819f19ef8c8e4eacf59f4eec1a188 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/Edit/NormalMapFixer.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 19db974e4dd5fe8438fb8e97b29bbea7 3 | folderAsset: yes 4 | DefaultImporter: 5 | externalObjects: {} 6 | userData: 7 | assetBundleName: 8 | assetBundleVariant: 9 | -------------------------------------------------------------------------------- /Editor/PixelWizards.Utilities.asmdef.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 48c78e8c1bdbac1459c1c47097f9a511 3 | AssemblyDefinitionImporter: 4 | externalObjects: {} 5 | userData: 6 | assetBundleName: 7 | assetBundleVariant: 8 | -------------------------------------------------------------------------------- /Editor/Edit/Shortcuts.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 688013518f228dc41b15f192d01517e6 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/LockToggle.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 43757114a34a46642b0171ef14b7711c 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/PhysicsSettler.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 0caaf3bfbc043c04180a2dffeced0640 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Assets/FindReference.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cc11561fbf2fdc6429c1e7461c788f8a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Assets/TextureCombiner.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 70fce3ae12011624796543d209f15082 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/CreateEmptyAtRoot.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: b9bd6f90a92c444488c09a84d4ac4937 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/DistributeEvenly.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 18c1138bca270b242b2bd1e755dbcc0f 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/SortSceneHierarchy.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 28abeab34004578488b48bdfa550b133 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/TextToTextMeshPro.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 9b4b8619845a1154dbebb51604386911 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Assets/BatchExtractMaterials.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: cd3af95c558ef0342baac8360ccce5d4 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/ReplaceSelectionWindow.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 17b3630307ca95d4e940e8d77b168854 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/ResetParentTransform.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 86a595b315b647f478dc9c40fe8469d8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Documentation~/Window/index.md: -------------------------------------------------------------------------------- 1 | # Window Utilities 2 | 3 | ## Sub Pages 4 | 5 | - Analysis 6 | - [Console Callstack Helper](Analysis/ConsoleCallstackHelper.md) 7 | - [Resource Checker](Analysis/ResourceChecker.md) 8 | - Sequencer 9 | - [Duplicate Timeline (with Binding)](Sequencer/DuplicateTimeline.md) 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Editor/Assets/Cleanup/RemoveEmptyFolders.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: f8830b53b0df87e4798ae9332b1f9962 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/NormalMapFixer/NormalFixer.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: bd68b17440662224bbcfeccc47ff4731 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/ReplaceMaterialInChildren.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5e6fd7b0ff71c7c44823442e46af9df8 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Window/Analysis/ResourceChecker.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 5e73da5ab4cf95248a4a820c3490e921 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Assets/Cleanup/UnityGuidRegenerator.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 761a98557f5771a4b9cea8729e5201ee 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Window/Analysis/ConsoleCallStackHelper.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 35c00d5d873f95948aa82e7607f38874 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Window/Sequencing/DuplicateTimeline.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: 54e3a473c046d9243a03095b0594287a 3 | MonoImporter: 4 | externalObjects: {} 5 | serializedVersion: 2 6 | defaultReferences: [] 7 | executionOrder: 0 8 | icon: {instanceID: 0} 9 | userData: 10 | assetBundleName: 11 | assetBundleVariant: 12 | -------------------------------------------------------------------------------- /Editor/Edit/GlobalDefinesWizard.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: ef7442d5657829c42b919199834fa541 3 | timeCreated: 1439589129 4 | licenseType: Store 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Editor/Assets/Cleanup/FindMissingScriptsRecursivelyAndRemove.cs.meta: -------------------------------------------------------------------------------- 1 | fileFormatVersion: 2 2 | guid: c22c767269cb772498a3617d336beda3 3 | timeCreated: 1506053631 4 | licenseType: Store 5 | MonoImporter: 6 | serializedVersion: 2 7 | defaultReferences: [] 8 | executionOrder: 0 9 | icon: {instanceID: 0} 10 | userData: 11 | assetBundleName: 12 | assetBundleVariant: 13 | -------------------------------------------------------------------------------- /Documentation~/Assets/Cleanup.md: -------------------------------------------------------------------------------- 1 | # Cleanup 2 | 3 | ## Overview 4 | 5 | This sub section represents the menu items located under the ***/Assets/Cleanup*** menu. 6 | 7 | ## Sub Pages 8 | 9 | - [Find (and Remove) Missing Scripts](Cleanup\FindMissingScripts.md) (and remove) 10 | - [Remove Empty Folders](Cleanup\RemoveEmptyFolders.md) 11 | - [Unity GUID Regenerator](Cleanup\UnityGUIDRegenerator.md) 12 | 13 | -------------------------------------------------------------------------------- /Documentation~/Edit/CreateEmptyatRoot.md: -------------------------------------------------------------------------------- 1 | # Create Empty at Root 2 | 3 | ## What it does 4 | 5 | Some times you just want to make a game object a child of another game object. 6 | 7 | That's what this does. 8 | 9 | ## How do you use this 10 | 11 | Similar to the 'Create Empty' option under the Game Object menu (or the right click context menu in the scene hierarchy), this option will add an empty game object and move the selected object underneath as a child game object. 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # com.pixelwizards.utilities 2 | # Overview 3 | 4 | Version 2, updated & Streamlined version of the original Pixel Wizards - Utilities Package 5 | 6 | ## What is Included 7 | 8 | 9 | 10 | ## Supported Unity versions 11 | 12 | This package should work with any LTS version of Unity (currently Unity 2021 and newer). 13 | 14 | # Documentation 15 | 16 | Read the [Documentation](Documentation~/index.md) for information on all of the individual utilities included in the package. 17 | 18 | # FAQ 19 | 20 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this package will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | # 0.2.0-preview.2 8 | replace batch package importer with proper window 9 | update resource checker for Unity 6 support 10 | add normal map fixer 11 | 12 | # 0.2.0-preview.1 13 | 14 | Version 2 initial version 15 | Reset & clean up of the original package 16 | -------------------------------------------------------------------------------- /Editor/PixelWizards.Utilities.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PixelWizards.Utilities", 3 | "rootNamespace": "", 4 | "references": [ 5 | "GUID:6055be8ebefd69e48b49212b09b47b2f", 6 | "GUID:478a2357cc57436488a56e564b08d223" 7 | ], 8 | "includePlatforms": [ 9 | "Editor" 10 | ], 11 | "excludePlatforms": [], 12 | "allowUnsafeCode": false, 13 | "overrideReferences": false, 14 | "precompiledReferences": [], 15 | "autoReferenced": true, 16 | "defineConstraints": [], 17 | "versionDefines": [], 18 | "noEngineReferences": false 19 | } -------------------------------------------------------------------------------- /Documentation~/Assets/index.md: -------------------------------------------------------------------------------- 1 | # Asset Utilities 2 | 3 | ## Overview 4 | This sub section represents the menu items located under the /Assets menu. 5 | 6 | ## Sub Pages 7 | - [Cleanup](Cleanup.md) 8 | - [FindMissingScripts](Cleanup/FindMissingScripts.md) 9 | - [Remove Empty Folders](Cleanup/RemoveEmptyFolders.md) 10 | - [Unity GUID Regenerator](Cleanup/UnityGUIDRegenerator.md) 11 | - [Batch Extract Materials](BatchExtractMaterials.md) 12 | - [Batch Import UnityPackage files](BatchImportUnityPackage.md) 13 | - [Find Reference](FindReference.md) 14 | - [Simple Texture Combiner](SimpleTextureCombiner.md) 15 | -------------------------------------------------------------------------------- /Documentation~/Edit/DistributeEvenly.md: -------------------------------------------------------------------------------- 1 | # Distribute Evenly 2 | 3 | ## What it does 4 | 5 | There are often times when you want to load a number of game objects into a scene at once but then be able to view the group of objects quickly. 6 | 7 | The distribution tools allow you to do this. 8 | 9 | ## How do you use this 10 | 11 | There are shortcuts that allow you to align objects along the X, Y or Z axis in the scene. 12 | 13 | Simply group select any number of objects and then press the keyboard short 14 | 15 | **Distribute along X** : 16 | 17 | **Distribute along Y** : 18 | 19 | **Distribute along Z** : 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Documentation~/Edit/index.md: -------------------------------------------------------------------------------- 1 | # Edit Utilities 2 | 3 | ## Overview 4 | This sub section represents the utilities located under the /Edit menu. 5 | 6 | ## Sub Pages 7 | 8 | - [Assorted Shortcuts](AssortedShortcuts.md) 9 | - [Create Empty at Root](CreateEmptyatRoot.md) 10 | - [Distribute Evenly](DistributeEvenly.md) 11 | - [Global Defines Wizard](GlobalDefinesWizard.md) 12 | - [Lock Toggle](LockToggle.md) 13 | - [Physics Settler](PhysicsSettler.md) 14 | - [Replace Material in Children](ReplaceMaterialinChildren.md) 15 | - [Replace Selection (Window)](ReplaceSelection.md) 16 | - [Sort Scene Hierarchy](SortSceneHierarchy.md) 17 | - [Text to Text Mesh Pro (Converter)](TexttoTextMeshPro.md) 18 | 19 | -------------------------------------------------------------------------------- /Editor/Edit/CreateEmptyAtRoot.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | namespace PixelWizards.Utilities 5 | { 6 | 7 | public class CreateEmptyAtRoot : MonoBehaviour 8 | { 9 | [MenuItem("GameObject/Create Empty at Root", false, 0)] 10 | public static void Create() 11 | { 12 | var go = new GameObject(); 13 | go.name = "GameObject"; 14 | go.transform.rotation = Quaternion.identity; 15 | go.transform.parent = Selection.activeTransform; 16 | go.transform.localPosition = Vector3.zero; 17 | go.transform.localRotation = Quaternion.Euler(0, 0, 0); 18 | go.transform.localScale = Vector3.one; 19 | Selection.activeObject = go; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Documentation~/Edit/SortSceneHierarchy.md: -------------------------------------------------------------------------------- 1 | # Sort by Name (Scene Hierarchy) 2 | 3 | ## What it does 4 | 5 | There are situations where you might want to re-order the objects in your scene hierarchy alphabetically. 6 | 7 | Unity DOES provide a custom sort mode (alphabetical) in the Hierarchy window, however this will break any UI elements in the scene (since Unity's UI uses the scene hierarchy for Z-depth sorting of the UI and doing a reorder will break your UI). 8 | 9 | This utility is designed to allow you to Sort specific sub-elements of a scene hierarchy alphabetically as desired. 10 | 11 | Do NOT use this on UI Canvas or children (and in fact it tries to avoid doing so) 12 | 13 | ## How do you use this 14 | 15 | Right click on any Game Object in your scene and choose 'Sort by Name' 16 | 17 | This will sort any children of that game object in the scene alphabetically. 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "com.pixelwizards.utilities", 3 | "displayName": "Pixel Wizards - Utilities", 4 | "version": "2.0.0-preview.1", 5 | "unity": "2018.4", 6 | "description": "A collection of quality of life utilities for Unity developers", 7 | "category": "Editor", 8 | "author": { 9 | "name": "Pixel Wizards", 10 | "email": "support@pixelwizards.ca", 11 | "url": "www.pixelwizards.ca" 12 | }, 13 | "dependencies": { 14 | "com.unity.textmeshpro": "3.0.9", 15 | "com.unity.editorcoroutines" : "1.0.0" 16 | }, 17 | "keywords": [ 18 | "Editor", 19 | "Utilities", 20 | "Quality of Life" 21 | ], 22 | "documentationUrl": "https://github.com/PixelWizards/com.pixelwizards.utilities/blob/main/Documentation~/index.md", 23 | "repository": { 24 | "url": "git@github.com:PixelWizards/com.pixelwizards.utilities.git", 25 | "type": "git" 26 | } 27 | } -------------------------------------------------------------------------------- /Documentation~/Window/Analysis/ResourceChecker.md: -------------------------------------------------------------------------------- 1 | # Resource Checker 2 | 3 | ## What it does 4 | 5 | The resource checker is a custom editor window that shows you a detailed breakdown of all of the textures, materials and meshes in any currently loaded scenes. 6 | 7 | It can be used in **edit mode** or at **runtime** to review any scenes are loaded. 8 | 9 | ## How do you use this 10 | 11 | Open ***Window -> Analysis -> Resource Checker*** 12 | 13 | The Resource Checker window will open 14 | 15 | ![](../../Images/ResourceChecker.png) 16 | 17 | Navigate to any of the tabs (Textures / Materials / Meshes) to view what is loaded in the current scene(s). 18 | 19 | You can click on any of the elements in the list to navigate to the object in the Project window. 20 | 21 | Each of the entries in the list also indicate dependencies, so you can see if a material is used by more than one objects etc. 22 | 23 | This is a very useful window for optimization and profiling your scene(s). 24 | 25 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## If you are interested in contributing, here are some ground rules: 4 | * First share your plan with us before starting to avoid redundant or disruptive work 5 | * External contributors can use Github issues for that 6 | * Branch of master for hot fixes, or dev for other works 7 | * Please include the ticket ID in your commits (either Jira or Github issue) 8 | 9 | 10 | ## All contributions are subject to the [Package License](LICENSE.md) 11 | By making a pull request, you are confirming agreement to the terms and conditions of the license, including that your Contributions are your original creation and that you have complete right and authority to make your Contributions. 12 | 13 | ## Once you have a change ready following these ground rules. 14 | * Before submitting anything, please clean your history (squash, fix-up commits) 15 | * Then create a pull request targeting dev branch and add the issue ID either in the title or the description of the PR. 16 | -------------------------------------------------------------------------------- /Documentation~/Assets/Cleanup/UnityGUIDRegenerator.md: -------------------------------------------------------------------------------- 1 | # Unity GUID Regenerator 2 | 3 | ## What it does 4 | 5 | Unity relies heavily on GUIDs to represent files and folders in your Unity project. 6 | 7 | There are situations when you might have content / packages that you are loading into a project with conflicting GUID files (for example, multiple packages from a single Asset Store vendor). 8 | 9 | This utility was created to help work around these issues by allowing you to regenerate the GUID files in your project. 10 | 11 | Note: this is an ALL OR NOTHING action. You can NOT generate GUIDS for only a single folder (for example) or file. 12 | 13 | ## TLDR: You probably don't need to do this 14 | 15 | If you don't know what the above means, then you don't need to do this. 16 | 17 | ## How do you use this 18 | 19 | Simply go to ***Assets -> Cleanup -> Regenerate GUIDs***. This will show you the following prompt: 20 | 21 | ![](../../Images/GUIDRegeneration.png) 22 | 23 | READ THE MESSAGE and then click 'Regenerate GUIDs' to continue. 24 | 25 | -------------------------------------------------------------------------------- /Documentation~/Assets/Cleanup/RemoveEmptyFolders.md: -------------------------------------------------------------------------------- 1 | # Remove Empty Folders 2 | 3 | ## What it does 4 | 5 | Remove Empty Folders enables or disables a task that runs in the background to delete any empty folders in your project. 6 | 7 | This can be useful if you delete a folder outside of Unity but potentially have not deleted the folder 'inside' Unity. This is a common situation if you are using Perforce for example. 8 | 9 | ## How do you use this 10 | 11 | Simply select the menu option ***'Assets -> Cleanup -> Remove Empty Folders***' to enable this behaviour. 12 | 13 | This will toggle the menu option from unchecked: 14 | 15 | ![](../../Images/RemoveEmptyFoldersDefault.png) 16 | 17 | To checked: 18 | 19 | ![](../../Images/RemoveEmptyFoldersChecked.png) 20 | 21 | Once enabled, this will monitor Asset Database changes and remove any Empty folders that it finds. 22 | 23 | *Note: this might cause frustration if enabled since it will delete any folders that is created manually as well (so you literally can't create folders if enabled).* 24 | 25 | -------------------------------------------------------------------------------- /Documentation~/Assets/Cleanup/FindMissingScripts.md: -------------------------------------------------------------------------------- 1 | # Find (and Remove) Missing Scripts 2 | 3 | ## What it does 4 | 5 | If you have ever worked on a large(ish) project, you have likely encountered a situation where you have a number of objects in your scene with missing / broken scripts. 6 | 7 | ## How do you use this 8 | 9 | Select any number of game objects in a scene (for example the parent game object in a hierarchy of objects), OR a group of prefabs in the Project window, and then open ***Assets -> Cleanup -> Find and Remove Missing Scripts*** 10 | 11 | This will open the following window: 12 | 13 | ![](../../Images/FindMissingScripts.png) 14 | 15 | Simply click 'Find Missing Scripts' and it will recurse through all of the objects and remove any missing scripts that exist. 16 | 17 | ## Roadmap / Todo: 18 | 19 | Add a 'confirm' option so it doesn't just auto remove all of the scripts. 20 | 21 | Note: this will 'just' remove the scripts from any prefabs in the scene, but you may still need to apply the changes to the prefab(s). 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Documentation~/Edit/ReplaceSelection.md: -------------------------------------------------------------------------------- 1 | # Replace Selection (Window) 2 | 3 | ## What it does 4 | 5 | There are many times when you need to replace an object (or group of objects) in a scene with another object, but want to keep the position of the existing objects while you do it. 6 | 7 | The Replace Selection window is designed to do exactly this. 8 | 9 | ## How do you use this 10 | 11 | 1) Select the objects in the Scene View that you want to replace 12 | 13 | 2. Open the menu **Edit -> Replace Selection** 14 | 15 | This will open the following dialog: 16 | 17 | ![](../Images/ReplaceSelection.png) 18 | 19 | Drag the new object you want to replace your selection WITH into the 'Replacement Object' field. 20 | 21 | 3. this will enable the 'Replace Selection' button. Simply click 'Replace Selection' and the objects in the scene that you have selected will be replaced with the object you specified. 22 | 23 | ![](../Images/ReplaceSelectionButton.png) 24 | 25 | You can repeat this operation any number of times as long as the window is open (including selecting different objects in the scene etc) 26 | 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2010-2025 Pixel Wizards. www.github.com/pixelwizards 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /Documentation~/Assets/BatchImportUnityPackage.md: -------------------------------------------------------------------------------- 1 | # Batch Import .unitypackage files 2 | 3 | ## What it does 4 | 5 | If you are like me, you buy a TON of stuff on the asset store. Even more so, when you find a vendor that you LIKE, you buy basically everything that they have. (don't judge me). 6 | 7 | However, when you are trying to load assets into a project, it is a very slow process to go through each package one by one and import them into the project, clicking through all of the dialogs etc. 8 | 9 | What if you could just point Unity at a folder with the .unitypackages in it and import them all at once? 10 | 11 | ## How do you use this 12 | 13 | Navigate to **Assets -> Batch Import .unitypackage files** 14 | 15 | The following dialog opens: 16 | 17 | ![](../Images/BatchImportPackages.png) 18 | 19 | Simply copy & paste the path that you want to import your packages from into the 'Package Path' field, choose whether you want to include all subdirectories and then click 'Import' 20 | 21 | For example: 22 | 23 | ```C:\Users\Mike\AppData\Roaming\Unity\Asset Store-5.x\My Favorite Vendor\``` 24 | 25 | I would highly suggest that you NOT try to import your entire Asset Store-5.x cache folder at once. This has a good chance of breaking Unity and will take a lot of disk space at best. 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Documentation~/Edit/TexttoTextMeshPro.md: -------------------------------------------------------------------------------- 1 | # Text to Text Mesh Pro (Converter) 2 | 3 | ## What it does 4 | 5 | Many moons ago, Unity added a new way to render text in Unity games (TextMeshPro). 6 | 7 | Unfortunately you can still encounter 3rd party packages / utils (for example, from the Asset Store) that use the legacy 'Text' component instead of it's replacement (TextMeshProUGUI). 8 | 9 | This utility will let you convert from the legacy Text components to the new TextMeshProUGUI components wherever they are found in a scene. 10 | 11 | ## How do you use this 12 | 13 | Select any game objects in a scene and then go to the menu ***Edit -> Text to Text Mesh Pro*** 14 | 15 | This will convert any Text components in the selection (and children) into the newer Text Mesh Pro UGUI components, copying any properties (font size / color etc) to the new component. 16 | 17 | ## Known Issues 18 | 19 | There are a variety of UI components that include a Text component, but also have a newer TextMeshPro variant. For example, Dropdown or Input Fields. 20 | 21 | This utility does NOT convert these from the legacy version into the TextMesh Pro version, and WILL convert any child game objects with the legacy Text component into the newer format (which in turn will break the legacy UI element as a result) 22 | 23 | **TLDR:** If you have any legacy Dropdown or Input Field elements in your UI, you should convert these manually into the newer Text Mesh Pro equivalents. 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Editor/Edit/ReplaceMaterialInChildren.cs: -------------------------------------------------------------------------------- 1 | /* 2 | * bulk update of materials in child objects 3 | * 4 | */ 5 | using UnityEngine; 6 | using UnityEditor; 7 | using System.Collections; 8 | 9 | namespace PixelWizards.Utilities 10 | { 11 | 12 | 13 | public class ReplaceMaterialInChildren : ScriptableWizard 14 | { 15 | public Material newMaterial = null; 16 | 17 | [MenuItem("Edit/Replace Materials in Children")] 18 | static void CreateWizard() 19 | { 20 | ScriptableWizard.DisplayWizard( 21 | "Replace Materials in Children", typeof(ReplaceMaterialInChildren), "Replace"); 22 | } 23 | 24 | void OnWizardCreate() 25 | { 26 | var transforms = Selection.GetTransforms( 27 | SelectionMode.TopLevel | SelectionMode.Editable); 28 | 29 | Undo.RegisterCompleteObjectUndo(transforms, "Replace Materials in Selection"); 30 | 31 | foreach (Transform t in transforms) 32 | { 33 | Debug.Log("Replacing all materials on object : " + t.name + " with " + newMaterial.name); 34 | 35 | var mrs = t.GetComponentsInChildren(); 36 | if (mrs != null) 37 | { 38 | foreach (var mr in mrs) 39 | { 40 | mr.sharedMaterial = newMaterial; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # This .gitignore file should be placed at the root of your Unity project directory 2 | # 3 | # Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore 4 | # 5 | /[Ll]ibrary/ 6 | /[Tt]emp/ 7 | /[Oo]bj/ 8 | /[Bb]uild/ 9 | /[Bb]uilds/ 10 | /[Ll]ogs/ 11 | /[Uu]ser[Ss]ettings/ 12 | 13 | # MemoryCaptures can get excessive in size. 14 | # They also could contain extremely sensitive data 15 | /[Mm]emoryCaptures/ 16 | 17 | # Recordings can get excessive in size 18 | /[Rr]ecordings/ 19 | 20 | # Uncomment this line if you wish to ignore the asset store tools plugin 21 | # /[Aa]ssets/AssetStoreTools* 22 | 23 | # Autogenerated Jetbrains Rider plugin 24 | /[Aa]ssets/Plugins/Editor/JetBrains* 25 | 26 | # Visual Studio cache directory 27 | .vs/ 28 | 29 | # Gradle cache directory 30 | .gradle/ 31 | 32 | # Autogenerated VS/MD/Consulo solution and project files 33 | ExportedObj/ 34 | .consulo/ 35 | *.csproj 36 | *.unityproj 37 | *.sln 38 | *.suo 39 | *.tmp 40 | *.user 41 | *.userprefs 42 | *.pidb 43 | *.booproj 44 | *.svd 45 | *.pdb 46 | *.mdb 47 | *.opendb 48 | *.VC.db 49 | 50 | # Unity3D generated meta files 51 | *.pidb.meta 52 | *.pdb.meta 53 | *.mdb.meta 54 | 55 | # Unity3D generated file on crash reports 56 | sysinfo.txt 57 | 58 | # Builds 59 | *.apk 60 | *.aab 61 | *.unitypackage 62 | *.app 63 | 64 | # Crashlytics generated file 65 | crashlytics-build.properties 66 | 67 | # Packed Addressables 68 | /[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin* 69 | 70 | # Temporary auto-generated Android Assets 71 | /[Aa]ssets/[Ss]treamingAssets/aa.meta 72 | /[Aa]ssets/[Ss]treamingAssets/aa/* 73 | -------------------------------------------------------------------------------- /Documentation~/Edit/AssortedShortcuts.md: -------------------------------------------------------------------------------- 1 | # Assorted Shortcuts 2 | 3 | ## What it does 4 | 5 | There are a number of other assorted shortcuts provided as a part of this package. 6 | 7 | The list of shortcuts are: 8 | 9 | ### Key Pad scene view tools 10 | 11 | **Key pad 7** - Scene view look straight down 12 | 13 | **Key pad 1** - Scene view look straight right 14 | 15 | **Key pad 3** - Scene view look straight forward (-z) 16 | 17 | **Key pad 5** - Scene view Reset 18 | 19 | **Key pad 0** - align Scene view with Main Camera in the scene 20 | 21 | 22 | 23 | ## Edit / Find in Project 24 | 25 | *Note: This is somewhat deprecated as Unity has added some much better search tools in recent versions.* 26 | 27 | This will look through all game objects in the project / scene and find any references to the selected game object. 28 | 29 | ## Group Tools 30 | 31 | ### Create Group 32 | 33 | The default is CTRL + G - this will great an empty parent game object and add the selected objects as children. 34 | 35 | *Note: one limitation is that all of the items to be grouped must be the same 'level' in the hierarchy. For example if you have 4 objects, but 2 are under one parent and 2 are under another parent, then the grouping will not work. If you are trying to do this, simply drag all of the parent game objects into the same position in the hierarchy and THEN group them.* 36 | 37 | ### Center Group on Children 38 | 39 | This attempts to center a parent game object based on the transform positions & bounding areas of it's children. 40 | 41 | ## Gizmos 42 | 43 | ### Enable all Gizmos 44 | 45 | Turns on all Gizmos (the same as clicking the 'show gizmos button' in the scene view) 46 | 47 | ### Disable all Gizmos 48 | 49 | Turns off all Gizmos (the same as clicking the 'hide gizmos button' in the scene view) 50 | -------------------------------------------------------------------------------- /Editor/Edit/ResetParentTransform.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEditor.ShortcutManagement; 4 | using UnityEngine; 5 | 6 | namespace PixelWizards.Utilities 7 | { 8 | public static class ResetParentTransform 9 | { 10 | [Shortcut("GameObject/Reset Parent Transforms", KeyCode.R, ShortcutModifiers.Alt)] 11 | public static void ResetParentTransforms() 12 | { 13 | var selectedObjects = Selection.gameObjects; 14 | Undo.RegisterCompleteObjectUndo(selectedObjects, "Reset Parent Transforms"); 15 | 16 | foreach (GameObject selectedObject in selectedObjects) 17 | { 18 | ResetChildTransforms(selectedObject.transform); 19 | } 20 | } 21 | 22 | private static void ResetChildTransforms(Transform parent) 23 | { 24 | // make a temp parent 25 | var newGo = new GameObject(); 26 | 27 | // grab all of the children 28 | var children = new List(); 29 | for (var i = 0; i < parent.childCount; i++) 30 | { 31 | children.Add(parent.GetChild(i)); 32 | } 33 | 34 | // move the children out of the parent temporarily 35 | foreach (var child in children) 36 | { 37 | child.parent = newGo.transform; 38 | } 39 | 40 | // reset the parent transform 41 | parent.position = Vector3.zero; 42 | parent.rotation = Quaternion.identity; 43 | 44 | // and reparent 45 | foreach (var child in children) 46 | { 47 | child.parent = parent; 48 | } 49 | 50 | GameObject.DestroyImmediate(newGo); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /Editor/Edit/NormalMapFixer/README.md: -------------------------------------------------------------------------------- 1 | # UnityNormalMapInverter 2 | Convert normal maps from DirectX (-y) to OpenGL (+y) 3 | 4 | "Sky in your Eye? Flip your Y" - doctorpangloss 5 | 6 | This is a simple tool for converting normal maps from DirectX style (-y) to OpenGL (+y). why are there 2 standards that contain the same information but have slightly different configuration? beats me. But ive seen too many people use the wrong type in the wrong engine, so I made this in-editor tool to fix it, which is much easier to use than opening gimp, decomposing the colors into the RGB components, inverting the G channel, recomposing, and overwriting. 7 | 8 | A render engine will use either DirectX or OpenGL style normals. Some common examples include: 9 | 10 | OpenGL: 11 | - Unity 12 | - Blender 13 | - Houdini 14 | - Maya 15 | - Zbrush 16 | - IClone 17 | 18 | DirectX: 19 | - UE4/5 20 | - Godot 21 | - CryEngine 22 | - Source Engine 23 | - Substance Designer/painter 24 | 25 | How do I know if my normal maps are fucked? 26 | Cause they look like this: 27 | ![FuckedOrNot](https://user-images.githubusercontent.com/59656122/162627338-a93b8efc-a28a-4a94-907a-1ec95cbeb385.png) 28 | 29 | 30 | 31 | HOW TO USE: 32 | 1) Open the NormalFixer Tool (Tools/NormalMapCorrecter) 33 | 34 | ![Screenshot_1](https://user-images.githubusercontent.com/59656122/162627605-31853625-b927-40e6-8de8-0a49481c41dd.png) 35 | 36 | 2) Drag the normal map into the only slot on the tool. Then press Invert Normal Map. (it might take a while to convert the map, depending on the size) 37 | 38 | ![Screenshot_2](https://user-images.githubusercontent.com/59656122/162627615-c6bf833f-543f-44cb-b52a-ffe1c36e546b.png) 39 | 40 | 3) Set the desired file destination + name. Click “save” 41 | 42 | ![Screenshot_10](https://user-images.githubusercontent.com/59656122/162627620-d5ee8fa5-20a9-4df7-8a99-3a132cc5fab7.png) 43 | -------------------------------------------------------------------------------- /Documentation~/index.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The Pixel Wizards Utilities package is a collection of useful tools, utilities and commands that are designed to improve the experience of working with / in the Unity Editor. 4 | 5 | ## Supported Versions 6 | 7 | The package is designed to work with any supported LTS version of Unity (as of this writing, Unity 2021 LTS and higher). 8 | 9 | # Documentation 10 | 11 | ## [Edit Utilities](Edit/index.md) 12 | 13 | - [Assorted Shortcuts](Edit/AssortedShortcuts.md) 14 | - [Create Empty at Root](Edit/CreateEmptyatRoot.md) 15 | - [Distribute Evenly](Edit/DistributeEvenly.md) 16 | - [Global Defines Wizard](Edit/GlobalDefinesWizard.md) 17 | - [Lock Toggle](Edit/LockToggle.md) 18 | - [Physics Settler](Edit/PhysicsSettler.md) 19 | - [Replace Material in Children](Edit/ReplaceMaterialinChildren.md) 20 | - [Replace Selection (Window)](Edit/ReplaceSelection.md) 21 | - [Sort Scene Hierarchy](Edit/SortSceneHierarchy.md) 22 | - [Text to Text Mesh Pro (Converter)](Edit/TexttoTextMeshPro.md) 23 | 24 | ## [Assets Utilities](Assets/index.md) 25 | 26 | - [Cleanup](Assets/Cleanup.md) 27 | - [FindMissingScripts](Assets/Cleanup/FindMissingScripts.md) 28 | - [Remove Empty Folders](Assets/Cleanup/RemoveEmptyFolders.md) 29 | - [Unity GUID Regenerator](Assets/Cleanup/UnityGUIDRegenerator.md) 30 | - [Batch Extract Materials](Assets/BatchExtractMaterials.md) 31 | - [Batch Import UnityPackage files](Assets/BatchImportUnityPackage.md) 32 | - [Find Reference](Assets/FindReference.md) 33 | - [Simple Texture Combiner](Assets/SimpleTextureCombiner.md) 34 | 35 | ## [Window Utilities](Window/index.md) 36 | 37 | - Analysis 38 | - [Console Callstack Helper](Window/Analysis/ConsoleCallstackHelper.md) 39 | - [Resource Checker](Window/Analysis/ResourceChecker.md) 40 | - Sequencer 41 | - [Duplicate Timeline (with Binding)](Window/Sequencer/DuplicateTimeline.md) 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.zip filter=lfs diff=lfs merge=lfs -text 2 | *.tga filter=lfs diff=lfs merge=lfs -text 3 | *.psd filter=lfs diff=lfs merge=lfs -text 4 | *.fbx filter=lfs diff=lfs merge=lfs -text 5 | *.png filter=lfs diff=lfs merge=lfs -text 6 | *.jpg filter=lfs diff=lfs merge=lfs -text 7 | *.unitypackage filter=lfs diff=lfs merge=lfs -text 8 | *.dll filter=lfs diff=lfs merge=lfs -text 9 | *.pdf filter=lfs diff=lfs merge=lfs -text 10 | *.sbsar filter=lfs diff=lfs merge=lfs -text 11 | *.wav filter=lfs diff=lfs merge=lfs -text 12 | *.ttf filter=lfs diff=lfs merge=lfs -text 13 | *.mdb filter=lfs diff=lfs merge=lfs -text 14 | *.pdb filter=lfs diff=lfs merge=lfs -text 15 | *.ogg filter=lfs diff=lfs merge=lfs -text 16 | *.tif filter=lfs diff=lfs merge=lfs -text 17 | *.cubemap filter=lfs diff=lfs merge=lfs -text 18 | *.spm filter=lfs diff=lfs merge=lfs -text 19 | *.bmp filter=lfs diff=lfs merge=lfs -text 20 | *.bin filter=lfs diff=lfs merge=lfs -text 21 | *.mb filter=lfs diff=lfs merge=lfs -text 22 | *.ma filter=lfs diff=lfs merge=lfs -text 23 | *.max filter=lfs diff=lfs merge=lfs -text 24 | *.blend filter=lfs diff=lfs merge=lfs -text 25 | *.exr filter=lfs diff=lfs merge=lfs -text 26 | *.au filter=lfs diff=lfs merge=lfs -text 27 | *.mp3 filter=lfs diff=lfs merge=lfs -text 28 | *.mask filter=lfs diff=lfs merge=lfs -text 29 | *.r16 filter=lfs diff=lfs merge=lfs -text 30 | *.raw filter=lfs diff=lfs merge=lfs -text 31 | *.uvw filter=lfs diff=lfs merge=lfs -text 32 | *.mixer filter=lfs diff=lfs merge=lfs -text 33 | *.eps filter=lfs diff=lfs merge=lfs -text 34 | *.chm filter=lfs diff=lfs merge=lfs -text 35 | *.mp4 filter=lfs diff=lfs merge=lfs -text 36 | *.asset filter=lfs diff=lfs merge=lfs -text 37 | *.flac filter=lfs diff=lfs merge=lfs -text 38 | *.scml filter=lfs diff=lfs merge=lfs -text 39 | *.unity filter=lfs diff=lfs merge=lfs -text 40 | *.ShaderGraph filter=lfs diff=lfs merge=lfs -text 41 | *.mov filter=lfs diff=lfs merge=lfs -text 42 | *.wim filter=lfs diff=lfs merge=lfs -text 43 | -------------------------------------------------------------------------------- /Editor/Window/Sequencing/DuplicateTimeline.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using UnityEngine.Playables; 4 | 5 | namespace PixelWizards.Utilities 6 | { 7 | public class DuplicateTimeline 8 | { 9 | [MenuItem("Window/Sequencing/Duplicate With Bindings", true)] 10 | public static bool DuplicateWithBindingsValidate() 11 | { 12 | if (UnityEditor.Selection.activeGameObject == null) 13 | return false; 14 | 15 | var playableDirector = UnityEditor.Selection.activeGameObject.GetComponent(); 16 | if (playableDirector == null) 17 | return false; 18 | 19 | var playableAsset = playableDirector.playableAsset; 20 | if (playableAsset == null) 21 | return false; 22 | 23 | var path = AssetDatabase.GetAssetPath(playableAsset); 24 | if (string.IsNullOrEmpty(path)) 25 | return false; 26 | 27 | return true; 28 | } 29 | 30 | [MenuItem("Window/Sequencing/Duplicate With Bindings")] 31 | public static void DuplicateWithBindings() 32 | { 33 | if (UnityEditor.Selection.activeGameObject == null) 34 | return; 35 | 36 | var playableDirector = UnityEditor.Selection.activeGameObject.GetComponent(); 37 | if (playableDirector == null) 38 | return; 39 | 40 | var playableAsset = playableDirector.playableAsset; 41 | if (playableAsset == null) 42 | return; 43 | 44 | var path = AssetDatabase.GetAssetPath(playableAsset); 45 | if (string.IsNullOrEmpty(path)) 46 | return; 47 | 48 | string newPath = path.Replace(".", "(Clone)."); 49 | if (!AssetDatabase.CopyAsset(path, newPath)) 50 | { 51 | Debug.LogError("Couldn't Clone Asset"); 52 | return; 53 | } 54 | 55 | var newPlayableAsset = AssetDatabase.LoadMainAssetAtPath(newPath) as PlayableAsset; 56 | var gameObject = GameObject.Instantiate(UnityEditor.Selection.activeGameObject); 57 | var newPlayableDirector = gameObject.GetComponent(); 58 | newPlayableDirector.playableAsset = newPlayableAsset; 59 | 60 | var oldBindings = playableAsset.outputs.GetEnumerator(); 61 | var newBindings = newPlayableAsset.outputs.GetEnumerator(); 62 | 63 | 64 | while (oldBindings.MoveNext()) 65 | { 66 | var oldBindings_sourceObject = oldBindings.Current.sourceObject; 67 | 68 | newBindings.MoveNext(); 69 | 70 | var newBindings_sourceObject = newBindings.Current.sourceObject; 71 | 72 | 73 | newPlayableDirector.SetGenericBinding( 74 | newBindings_sourceObject, 75 | playableDirector.GetGenericBinding(oldBindings_sourceObject) 76 | ); 77 | } 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /Editor/Edit/SortSceneHierarchy.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | 4 | namespace PixelWizards.Utilities 5 | { 6 | /// 7 | /// Simple script to order a single level of your scene hierarchy alphabetically. Right Click on a GameObject in your scene and 'Sort by Name' 8 | /// 9 | /// Note: you MUST not do this with UI elements because sorting == draw order and this will fuck it up. 10 | /// 11 | /// Source: https://answers.unity.com/questions/717398/hierarchy-is-not-in-order.html?_ga=2.161714364.1693018819.1602479154-717108197.1594613165 12 | /// 13 | public class SortChildObjects : EditorWindow 14 | { 15 | 16 | [MenuItem("GameObject/Sort By Name", false, -1)] 17 | public static void SortGameObjectsByName(MenuCommand menuCommand) 18 | { 19 | if (menuCommand.context == null || menuCommand.context.GetType() != typeof(GameObject)) 20 | { 21 | EditorUtility.DisplayDialog("Error", "You must select an item to sort in the frame", "Okay"); 22 | return; 23 | } 24 | 25 | var parentObject = (GameObject)menuCommand.context; 26 | 27 | if (parentObject.GetComponentInChildren()) 28 | { 29 | EditorUtility.DisplayDialog("Error", "You are trying to sort a GUI element. This will screw up EVERYTHING, do not do", "Okay"); 30 | return; 31 | } 32 | 33 | // Build a list of all the Transforms in this player's hierarchy 34 | var objectTransforms = new Transform[parentObject.transform.childCount]; 35 | for (var i = 0; i < objectTransforms.Length; i++) 36 | { 37 | objectTransforms[i] = parentObject.transform.GetChild(i); 38 | } 39 | 40 | var sortTime = System.Environment.TickCount; 41 | 42 | var sorted = false; 43 | // Perform a bubble sort on the objects 44 | while (sorted == false) 45 | { 46 | sorted = true; 47 | for (var i = 0; i < objectTransforms.Length - 1; i++) 48 | { 49 | // Compare the two strings to see which is sooner 50 | var comparison = objectTransforms[i].name.CompareTo(objectTransforms[i + 1].name); 51 | 52 | if (comparison <= 0) 53 | { 54 | continue; // 1 means that the current value is larger than the last value 55 | } 56 | 57 | objectTransforms[i].transform.SetSiblingIndex(objectTransforms[i + 1].GetSiblingIndex()); 58 | sorted = false; 59 | } 60 | 61 | // resort the list to get the new layout 62 | for (var i = 0; i < objectTransforms.Length; i++) 63 | { 64 | objectTransforms[i] = parentObject.transform.GetChild(i); 65 | } 66 | } 67 | 68 | Debug.Log("Sort took " + (System.Environment.TickCount - sortTime) + " milliseconds"); 69 | 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Editor/Assets/Cleanup/FindMissingScriptsRecursivelyAndRemove.cs: -------------------------------------------------------------------------------- 1 | #if UNITY_EDITOR && NET_4_6 2 | using UnityEditor; 3 | using UnityEngine; 4 | 5 | namespace PixelWizards.Utilities 6 | { 7 | /// from this pastebin: https://pastebin.com/DLuE2Ze9 8 | public class FindMissingScriptsRecursivelyAndRemove : EditorWindow 9 | { 10 | private static int _goCount; 11 | private static int _componentsCount; 12 | private static int _missingCount; 13 | 14 | private static bool _bHaveRun; 15 | 16 | [MenuItem("Assets/Cleanup/Find and Remove Missing Scripts", false, 1000)] 17 | public static void ShowWindow() 18 | { 19 | GetWindow(typeof(FindMissingScriptsRecursivelyAndRemove)); 20 | } 21 | 22 | public void OnGUI() 23 | { 24 | if (GUILayout.Button("Find Missing Scripts in selected GameObjects")) 25 | { 26 | FindInSelected(); 27 | } 28 | 29 | if (!_bHaveRun) return; 30 | 31 | EditorGUILayout.TextField($"{_goCount} GameObjects Selected"); 32 | if(_goCount>0) EditorGUILayout.TextField($"{_componentsCount} Components"); 33 | if(_goCount>0) EditorGUILayout.TextField($"{_missingCount} Deleted"); 34 | } 35 | 36 | private static void FindInSelected() 37 | { 38 | var go = Selection.gameObjects; 39 | _goCount = 0; 40 | _componentsCount = 0; 41 | _missingCount = 0; 42 | foreach (var g in go) 43 | { 44 | FindInGo(g); 45 | } 46 | 47 | _bHaveRun = true; 48 | Debug.Log($"Searched {_goCount} GameObjects, {_componentsCount} components, found {_missingCount} missing"); 49 | 50 | AssetDatabase.SaveAssets(); 51 | } 52 | 53 | private static void FindInGo(GameObject g) 54 | { 55 | _goCount++; 56 | var components = g.GetComponents(); 57 | 58 | var r = 0; 59 | 60 | for (var i = 0; i < components.Length; i++) 61 | { 62 | _componentsCount++; 63 | if (components[i] != null) continue; 64 | _missingCount++; 65 | var s = g.name; 66 | var t = g.transform; 67 | while (t.parent != null) 68 | { 69 | s = t.parent.name +"/"+s; 70 | t = t.parent; 71 | } 72 | 73 | Debug.Log ($"{s} has a missing script at {i}", g); 74 | 75 | var serializedObject = new SerializedObject(g); 76 | 77 | var prop = serializedObject.FindProperty("m_Component"); 78 | 79 | prop.DeleteArrayElementAtIndex(i-r); 80 | r++; 81 | 82 | serializedObject.ApplyModifiedProperties(); 83 | } 84 | 85 | foreach (Transform childT in g.transform) 86 | { 87 | FindInGo(childT.gameObject); 88 | } 89 | } 90 | } 91 | } 92 | #endif -------------------------------------------------------------------------------- /Documentation~/Assets/BatchExtractMaterials.md: -------------------------------------------------------------------------------- 1 | # Batch Extract Materials 2 | 3 | ## What it does 4 | 5 | When working with large groups of objects, it is very common to want to extract all of the materials for the objects. Unfortunately Unity does not support batch editing of a selection of objects and, by default, requires that you manually select and manage the materials one by one. 6 | 7 | ![](../Images/materialmultiedit.png) 8 | 9 | This is very time consuming and painful when working with dozens of objects, which is why the Batch Extract Materials wizard was created. 10 | 11 | ## How do you use this 12 | 13 | Navigate to **Assets -> Batch Extract Materials** 14 | 15 | This will open this window 16 | 17 | ![](../Images/BatchExtractWindow.png) 18 | 19 | Simply drag and drop source meshes (not prefabs) onto the 'Models to Process' area (you can drag multiple objects) to add them to the list. This will populate the list, like so: 20 | 21 | ![](../Images/MultipleObjectsBatchMaterial.png) 22 | 23 | ## Material Remap Options 24 | 25 | There are a number of options that can be used while remapping the materials. 26 | 27 | ### Material Names must match 28 | 29 | This is used in the validation process. In order to re-use materials between different meshes, this ensures that the material names match. For example "Material A", will not match "Material B" from another object, but if both objects have "Material A", then they match and both objects will use the same common material. 30 | 31 | ### Material properties must match 32 | 33 | This is used in the validation process. In order to re-use materials between different meshes, this ensures that the material properties match. For example if ObjectA has a material with base texture "TextureA", it will not match another object with a base texture "TextureB", but if both objects have have a material with base texture "TextureA", then they match and both objects will use the same common material. 34 | 35 | ### Don't remap already extracted materials 36 | 37 | If you have extracted materials from an object previously, this will set the remapping to ignore these materials. 38 | 39 | ### Don't map Model A's materials to Model B 40 | 41 | This checkbox basically ensures that all objects will have unique materials, even if the above rules match. 42 | 43 | ## Verify the Remap 44 | 45 | Once you click 'next' on the dialog above, the system will iterate through all of the objects in the list and provide a list of all of teh materials and show you what the system has determined the best option for each material will be. 46 | 47 | For example: 48 | 49 | ![](../Images/VerifyExtractMaterials.png) 50 | 51 | The top buttons let you alternately specify whether you want to 'extract' all of the materials (make unique), 'remap' them all (try to create a common material set that is reused) OR ignore all, which effectively does nothing. 52 | 53 | As you can see from the image above, you can also modify individual materials in the list if you so choose (so if the system wants to 'remap' a material you could choose to 'extract' it to a unique material for example). 54 | 55 | Once you are happy with the intended results, simply click 'Extract!' and the system will run the operation, or hit 'Back' if you want to change settings on a prior screen. 56 | 57 | That's it! The materials will be extracted into the folder you specified! 58 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at rizasif@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /Editor/Assets/Cleanup/RemoveEmptyFolders.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Text; 6 | using System.Linq; 7 | using System; 8 | 9 | namespace PixelWizards.Utilities 10 | { 11 | /// 12 | /// Remove empty folders automatically. Borrowed from https://gist.github.com/mob-sakai/b98a62c1f2c94c1fef9021101635a5cd 13 | /// 14 | public class RemoveEmptyFolders : UnityEditor.AssetModificationProcessor 15 | { 16 | public const string kMenuText = "Assets/Cleanup/Remove Empty Folders"; 17 | static readonly StringBuilder s_Log = new StringBuilder(); 18 | static readonly List s_Results = new List(); 19 | 20 | /// 21 | /// Raises the initialize on load method event. 22 | /// 23 | [InitializeOnLoadMethod] 24 | static void OnInitializeOnLoadMethod() 25 | { 26 | EditorApplication.delayCall += () => Valid(); 27 | } 28 | 29 | /// 30 | /// Raises the will save assets event. 31 | /// 32 | static string[] OnWillSaveAssets(string[] paths) 33 | { 34 | // If menu is unchecked, do nothing. 35 | if (!EditorPrefs.GetBool(kMenuText, false)) 36 | return paths; 37 | 38 | // Get empty directories in Assets directory 39 | s_Results.Clear(); 40 | var assetsDir = Application.dataPath + Path.DirectorySeparatorChar; 41 | GetEmptyDirectories(new DirectoryInfo(assetsDir), s_Results); 42 | 43 | // When empty directories has detected, remove the directory. 44 | if (0 < s_Results.Count) 45 | { 46 | s_Log.Length = 0; 47 | s_Log.AppendFormat("Remove {0} empty directories as following:\n", s_Results.Count); 48 | foreach (var d in s_Results) 49 | { 50 | s_Log.AppendFormat("- {0}\n", d.FullName.Replace(assetsDir, "")); 51 | FileUtil.DeleteFileOrDirectory(d.FullName); 52 | } 53 | // UNITY BUG: Debug.Log can not set about more than 15000 characters. 54 | s_Log.Length = Mathf.Min(s_Log.Length, 15000); 55 | Debug.Log(s_Log.ToString()); 56 | s_Log.Length = 0; 57 | 58 | Debug.Log("NOTE: Enable or Disable this behaviour under Assets -> Cleanup -> Remove Empty Folders"); 59 | 60 | AssetDatabase.Refresh(); 61 | } 62 | return paths; 63 | } 64 | 65 | /// 66 | /// Toggles the menu. 67 | /// 68 | [MenuItem(kMenuText)] 69 | static void OnClickMenu() 70 | { 71 | // Check/Uncheck menu. 72 | bool isChecked = !Menu.GetChecked(kMenuText); 73 | Menu.SetChecked(kMenuText, isChecked); 74 | 75 | // Save to EditorPrefs. 76 | EditorPrefs.SetBool(kMenuText, isChecked); 77 | 78 | Debug.Log("Will remove Empty Folders : " + isChecked); 79 | 80 | OnWillSaveAssets(null); 81 | } 82 | 83 | [MenuItem(kMenuText, true)] 84 | static bool Valid() 85 | { 86 | // Check/Uncheck menu from EditorPrefs. 87 | Menu.SetChecked(kMenuText, EditorPrefs.GetBool(kMenuText, false)); 88 | return true; 89 | } 90 | 91 | /// 92 | /// Get empty directories. 93 | /// 94 | static bool GetEmptyDirectories(DirectoryInfo dir, List results) 95 | { 96 | bool isEmpty = true; 97 | try 98 | { 99 | isEmpty = dir.GetDirectories().Count(x => !GetEmptyDirectories(x, results)) == 0 // Are sub directories empty? 100 | && dir.GetFiles("*.*").All(x => x.Extension == ".meta"); // No file exist? 101 | } 102 | catch 103 | { 104 | } 105 | 106 | // Store empty directory to results. 107 | if (isEmpty) 108 | results.Add(dir); 109 | return isEmpty; 110 | } 111 | } 112 | } -------------------------------------------------------------------------------- /Editor/Edit/DistributeEvenly.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEditor.ShortcutManagement; 3 | using UnityEngine; 4 | 5 | namespace PixelWizards.Utilities 6 | { 7 | public static class DistributeEvenly 8 | { 9 | public enum DistMode 10 | { 11 | X, 12 | Y, 13 | Z, 14 | } 15 | 16 | [MenuItem("Edit/Distribute/Along X", false, 2)] 17 | #if UNITY_2019_1_OR_NEWER 18 | [Shortcut("Edit/Distribute/Along X", KeyCode.Period, ShortcutModifiers.Action)] 19 | #endif 20 | public static void DistributeEvenlyX() 21 | { 22 | DistributeEvenlyInternal(DistMode.X); 23 | } 24 | 25 | [MenuItem("Edit/Distribute/Along Y", false, 2)] 26 | #if UNITY_2019_1_OR_NEWER 27 | [Shortcut("Edit/Distribute/Along Y", KeyCode.Slash, ShortcutModifiers.Action)] 28 | #endif 29 | public static void DistributeEvenlyY() 30 | { 31 | DistributeEvenlyInternal(DistMode.Y); 32 | } 33 | 34 | [MenuItem("Edit/Distribute/Along Z", false, 2)] 35 | #if UNITY_2019_1_OR_NEWER 36 | [Shortcut("Edit/Distribute/Along Z", KeyCode.Comma, ShortcutModifiers.Action)] 37 | #endif 38 | public static void DistributeEvenlyZ() 39 | { 40 | DistributeEvenlyInternal(DistMode.Z); 41 | } 42 | 43 | private static void DistributeEvenlyInternal(DistMode mode) 44 | { 45 | var selectedObjects = Selection.gameObjects; 46 | if (selectedObjects.Length < 1) 47 | return; 48 | 49 | var sourcePosition = selectedObjects[0]; 50 | Undo.RegisterCompleteObjectUndo(selectedObjects, "Distribute Evenly"); 51 | 52 | var previousSize = Vector3.zero; 53 | Vector3 nextPos = sourcePosition.transform.position; 54 | foreach (var selectedObject in selectedObjects) 55 | { 56 | var collider = selectedObject.AddComponent(); 57 | if (selectedObject.transform.childCount > 0) 58 | GrowToFitChildren(selectedObject, selectedObject); 59 | // bump the subsequent objects 60 | if( selectedObject != selectedObjects[0]) 61 | { 62 | nextPos += collider.bounds.extents; 63 | nextPos.x += (collider.bounds.extents.x * 0.25f); // add a bit of buffer between them 64 | } 65 | 66 | switch( mode ) 67 | { 68 | case DistMode.X: 69 | { 70 | selectedObject.transform.position = new Vector3(nextPos.x, sourcePosition.transform.position.y, sourcePosition.transform.position.z); 71 | 72 | break; 73 | } 74 | case DistMode.Y: 75 | { 76 | selectedObject.transform.position = new Vector3(sourcePosition.transform.position.x, nextPos.y, sourcePosition.transform.position.z); 77 | 78 | break; 79 | } 80 | case DistMode.Z: 81 | { 82 | selectedObject.transform.position = new Vector3(sourcePosition.transform.position.x, sourcePosition.transform.position.y, nextPos.z); 83 | 84 | break; 85 | } 86 | } 87 | nextPos = selectedObject.transform.position + collider.bounds.extents; 88 | 89 | GameObject.DestroyImmediate(collider); 90 | } 91 | } 92 | 93 | public static void GrowToFitChildren(GameObject thisGO, GameObject parent) 94 | { 95 | var collider = parent.GetComponent(); 96 | // need to take into account the size of any children as well 97 | foreach (Transform child in thisGO.transform) 98 | { 99 | var childCollider = child.gameObject.AddComponent(); 100 | var bounds = collider.bounds; 101 | bounds.Encapsulate(childCollider.bounds); 102 | collider.size = bounds.size; 103 | GameObject.DestroyImmediate(childCollider); 104 | 105 | if( child.childCount > 0) 106 | { 107 | GrowToFitChildren(child.gameObject, parent); 108 | } 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /Editor/Edit/PhysicsSettler.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using UnityEditor.ShortcutManagement; 4 | 5 | namespace PixelWizards.Utilities 6 | { 7 | // This causes the class' static constructor to be called on load and on starting playmode 8 | [InitializeOnLoad] 9 | class PhysicsSettler 10 | { 11 | // only ever register once 12 | static bool registered = false; 13 | 14 | // are we actively settling physics in our scene 15 | static bool active = false; 16 | 17 | // the work list of rigid bodies we can find loaded up 18 | static Rigidbody[] workList; 19 | 20 | // we need to disable auto simulation to manually tick physics 21 | static SimulationMode cachedAutoSimulation; 22 | 23 | // how long do we run physics for before we give up getting things to sleep 24 | const float timeToSettle = 5f; 25 | 26 | // how long have we been running 27 | static float activeTime = 0f; 28 | 29 | // this is the static constructor called by [InitializeOnLoad] 30 | static PhysicsSettler() 31 | { 32 | if (!registered) 33 | { 34 | // hook into the editor update 35 | EditorApplication.update += Update; 36 | 37 | // and the scene view OnGui 38 | SceneView.duringSceneGui += OnSceneGUI; 39 | registered = true; 40 | } 41 | } 42 | 43 | // let users turn on 44 | [Shortcut("Edit/Settle Physics", KeyCode.Q, ShortcutModifiers.Action | ShortcutModifiers.Alt)] 45 | static void Activate() 46 | { 47 | if (!active) 48 | { 49 | active = true; 50 | 51 | // Normally avoid Find functions, but this is editor time and only happens once 52 | workList = (Rigidbody[])Object.FindObjectsByType(typeof(Rigidbody), FindObjectsSortMode.None); 53 | 54 | // we will need to ensure autoSimulation is off to manually tick physics 55 | cachedAutoSimulation = Physics.simulationMode; 56 | activeTime = 0f; 57 | 58 | // make sure that all rigidbodies are awake so they will actively settle against changed geometry. 59 | foreach (Rigidbody body in workList) 60 | { 61 | body.WakeUp(); 62 | } 63 | } 64 | } 65 | 66 | // grey out the menu item while we are settling physics 67 | [MenuItem("Edit/Settle Physics", true)] 68 | static bool checkMenu() 69 | { 70 | return !active; 71 | } 72 | 73 | static void Update() 74 | { 75 | if (active) 76 | { 77 | activeTime += Time.deltaTime; 78 | 79 | // make sure we are not autosimulating 80 | Physics.simulationMode = SimulationMode.Script; 81 | 82 | // see if all our 83 | bool allSleeping = true; 84 | foreach (Rigidbody body in workList) 85 | { 86 | if (body != null) 87 | { 88 | allSleeping &= body.IsSleeping(); 89 | } 90 | } 91 | 92 | if (allSleeping || activeTime >= timeToSettle) 93 | { 94 | Physics.simulationMode = cachedAutoSimulation; 95 | active = false; 96 | } 97 | else 98 | { 99 | Physics.Simulate(Time.deltaTime); 100 | } 101 | } 102 | } 103 | 104 | static void OnSceneGUI(SceneView sceneView) 105 | { 106 | if (active) 107 | { 108 | Handles.BeginGUI(); 109 | Color cacheColor = GUI.color; 110 | GUI.color = Color.red; 111 | GUILayout.Label("Simulating Physics.", GUI.skin.box, GUILayout.Width(200)); 112 | GUILayout.Label(string.Format("Time Remaining: {0:F2}", (timeToSettle - activeTime)), GUI.skin.box, GUILayout.Width(200)); 113 | Handles.EndGUI(); 114 | 115 | foreach (Rigidbody body in workList) 116 | { 117 | if (body != null) 118 | { 119 | bool isSleeping = body.IsSleeping(); 120 | if (!isSleeping) 121 | { 122 | GUI.color = Color.green; 123 | Handles.Label(body.transform.position, "SIMULATING"); 124 | } 125 | } 126 | } 127 | GUI.color = cacheColor; 128 | } 129 | } 130 | } 131 | } -------------------------------------------------------------------------------- /Editor/Edit/LockToggle.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using UnityEditor; 4 | #if UNITY_2019_1_OR_NEWER 5 | using UnityEditor.ShortcutManagement; 6 | #endif 7 | using UnityEngine; 8 | 9 | /// 10 | /// Inspired by https://forum.unity.com/threads/shortcut-key-for-lock-inspector.95815/#post-5013983 11 | /// modded from: https://github.com/rfadeev/pump-editor/wiki/Lock-Toggle-Shortcuts 12 | /// 13 | /// Provides toggle locking for various windows in the editor: 14 | /// - Scene Hierarchy 15 | /// - Project Window 16 | /// - Inspector 17 | /// - Timeline Window 18 | /// 19 | namespace PixelWizards.Utilities 20 | { 21 | public static class LockToggle 22 | { 23 | [Shortcut("Window/Toggle Lock Focused Window", KeyCode.W, ShortcutModifiers.Action)] 24 | private static void ToggleLockFocusedWindow() 25 | { 26 | ToggleLockEditorWindow(EditorWindow.focusedWindow); 27 | } 28 | 29 | 30 | [Shortcut("Window/Toggle Lock Mouse Over Window", KeyCode.E, ShortcutModifiers.Action)] 31 | private static void ToggleLockMouseOverWindow() 32 | { 33 | ToggleLockEditorWindow(EditorWindow.mouseOverWindow); 34 | } 35 | 36 | [Shortcut("Window/Toggle Lock All Windows", KeyCode.W, ShortcutModifiers.Action | ShortcutModifiers.Shift)] 37 | private static void ToggleLockAllWindows() 38 | { 39 | var allWindows = Resources.FindObjectsOfTypeAll(); 40 | foreach (var editorWindow in allWindows) 41 | { 42 | ToggleLockEditorWindow(editorWindow); 43 | } 44 | } 45 | 46 | private static void ToggleLockEditorWindow(EditorWindow editorWindow) 47 | { 48 | var editorAssembly = Assembly.GetAssembly(typeof(Editor)); 49 | var projectBrowserType = editorAssembly.GetType("UnityEditor.ProjectBrowser"); 50 | var inspectorWindowType = editorAssembly.GetType("UnityEditor.InspectorWindow"); 51 | var sceneHierarchyWindowType = editorAssembly.GetType("UnityEditor.SceneHierarchyWindow"); 52 | var timelineWindowType = Type.GetType("UnityEditor.Timeline.TimelineWindow, Unity.Timeline.Editor"); 53 | 54 | var editorWindowType = editorWindow.GetType(); 55 | if (editorWindowType == projectBrowserType) 56 | { 57 | // Unity C# reference: https://github.com/Unity-Technologies/UnityCsReference/blob/c6ec7823//Editor/Mono/ProjectBrowser.cs#L113 58 | var propertyInfo = projectBrowserType.GetProperty("isLocked", BindingFlags.Instance | BindingFlags.NonPublic); 59 | 60 | var value = (bool)propertyInfo.GetValue(editorWindow); 61 | propertyInfo.SetValue(editorWindow, !value); 62 | } 63 | else if (editorWindowType == inspectorWindowType) 64 | { 65 | // Unity C# reference: https://github.com/Unity-Technologies/UnityCsReference/blob/c6ec7823//Editor/Mono/Inspector/InspectorWindow.cs##L492 66 | var propertyInfo = inspectorWindowType.GetProperty("isLocked"); 67 | 68 | var value = (bool)propertyInfo.GetValue(editorWindow); 69 | propertyInfo.SetValue(editorWindow, !value); 70 | } 71 | else if (editorWindowType == sceneHierarchyWindowType) 72 | { 73 | // Unity C# reference: https://github.com/Unity-Technologies/UnityCsReference/blob/c6ec7823/Editor/Mono/SceneHierarchyWindow.cs#L34 74 | var sceneHierarchyPropertyInfo = sceneHierarchyWindowType.GetProperty("sceneHierarchy"); 75 | var sceneHierarchy = sceneHierarchyPropertyInfo.GetValue(editorWindow); 76 | 77 | // Unity C# reference: https://github.com/Unity-Technologies/UnityCsReference/blob/c6ec7823/Editor/Mono/SceneHierarchy.cs#L88 78 | var sceneHierarchyType = editorAssembly.GetType("UnityEditor.SceneHierarchy"); 79 | var propertyInfo = sceneHierarchyType.GetProperty("isLocked", BindingFlags.Instance | BindingFlags.NonPublic); 80 | 81 | var value = (bool)propertyInfo.GetValue(sceneHierarchy); 82 | propertyInfo.SetValue(sceneHierarchy, !value); 83 | } 84 | else if ( editorWindowType == timelineWindowType) 85 | { 86 | var timelineWindow = Resources.FindObjectsOfTypeAll(timelineWindowType)[0] as EditorWindow; 87 | var propertyInfo = timelineWindow.GetType().GetProperty("locked"); 88 | var value = (bool)propertyInfo.GetValue(timelineWindow, null); 89 | propertyInfo.SetValue(timelineWindow, !value, null); 90 | } 91 | else 92 | { 93 | return; 94 | } 95 | 96 | editorWindow.Repaint(); 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Editor/Window/Analysis/ConsoleCallStackHelper.cs: -------------------------------------------------------------------------------- 1 | // MIT License 2 | // 3 | // Copyright (c) 2018 Sabresaurus 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | 23 | using System; 24 | using System.Reflection; 25 | using UnityEditor; 26 | using UnityEngine; 27 | 28 | namespace PixelWizards.Utilities 29 | { 30 | 31 | public class ConsoleCallStackHelper : EditorWindow 32 | { 33 | Vector2 scrollPosition = Vector2.zero; 34 | 35 | [MenuItem("Window/Analysis/Call Stack", false, 0)] 36 | static void Init() 37 | { 38 | ConsoleCallStackHelper window = EditorWindow.GetWindow(); 39 | window.Show(); 40 | window.titleContent = new GUIContent("Call Stack"); 41 | } 42 | 43 | private void OnGUI() 44 | { 45 | Color backgroundColor = GUI.backgroundColor; 46 | Type consoleWindowType = Type.GetType("UnityEditor.ConsoleWindow, UnityEditor", true); 47 | UnityEngine.Object[] windows = Resources.FindObjectsOfTypeAll(consoleWindowType); 48 | if (windows.Length > 0) 49 | { 50 | // Fetch the active callstack from the console window 51 | FieldInfo fieldInfo = consoleWindowType.GetField("m_ActiveText", BindingFlags.Instance | BindingFlags.NonPublic); 52 | string output = (string)fieldInfo.GetValue(windows[0]); 53 | 54 | GUIStyle style = new GUIStyle(GUI.skin.label); 55 | style.wordWrap = true; 56 | style.richText = true; 57 | 58 | style.normal.background = EditorGUIUtility.whiteTexture; 59 | 60 | scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); 61 | GUI.backgroundColor = new Color(1, 1, 1, 0.25f); 62 | 63 | // Split the callstack into lines 64 | string[] lines = output.Split('\n'); 65 | foreach (string line in lines) 66 | { 67 | string displayLine = line; 68 | int firstIndex = line.LastIndexOf(" (at Assets/", StringComparison.InvariantCultureIgnoreCase); 69 | // Wrap the line in bold tags 70 | if (firstIndex != -1) 71 | { 72 | displayLine = displayLine.Insert(firstIndex + 5, ""); 73 | displayLine = displayLine.Insert(displayLine.Length - 1, ""); 74 | } 75 | // Click the line if valid 76 | if (GUILayout.Button(displayLine, style)) 77 | { 78 | if (firstIndex != -1) 79 | { 80 | // Valid target, jump to the line in the file 81 | string trimmed = line.Substring(firstIndex + 5, line.Length - firstIndex - 6); 82 | string[] splitLine = trimmed.Split(':'); 83 | AssetDatabase.OpenAsset(AssetDatabase.LoadMainAssetAtPath(splitLine[0]), int.Parse(splitLine[1])); 84 | } 85 | } 86 | // Show a rollover cursor on valid targets 87 | if (firstIndex != -1) 88 | { 89 | Rect rect = GUILayoutUtility.GetLastRect(); 90 | EditorGUIUtility.AddCursorRect(rect, MouseCursor.Link); 91 | } 92 | } 93 | GUI.backgroundColor = backgroundColor; 94 | 95 | EditorGUILayout.EndScrollView(); 96 | } 97 | } 98 | 99 | void OnInspectorUpdate() 100 | { 101 | // Repaint 10 times a second in case they clicked a new log entry 102 | Repaint(); 103 | } 104 | } 105 | } -------------------------------------------------------------------------------- /Editor/Edit/Shortcuts.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using System.Collections.Generic; 4 | using UnityEditor.ShortcutManagement; 5 | 6 | namespace PixelWizards.Utilities 7 | { 8 | [InitializeOnLoad] 9 | public class Shortcuts 10 | { 11 | static Shortcuts() 12 | { 13 | SceneView.duringSceneGui += CheckSceneShortcuts; 14 | } 15 | 16 | static void CheckSceneShortcuts(SceneView view) 17 | { 18 | Event evt = Event.current; 19 | if (evt.type == EventType.KeyDown) 20 | { 21 | switch (evt.keyCode) 22 | { 23 | case KeyCode.Keypad7: 24 | { 25 | view.LookAt(view.pivot, Quaternion.LookRotation(Vector3.down, Vector3.forward), view.size, 26 | true); 27 | evt.Use(); 28 | break; 29 | } 30 | case KeyCode.Keypad1: 31 | { 32 | view.LookAt(view.pivot, Quaternion.LookRotation(Vector3.right, Vector3.up), view.size, true); 33 | evt.Use(); 34 | break; 35 | } 36 | case KeyCode.Keypad3: 37 | { 38 | view.LookAt(view.pivot, Quaternion.LookRotation(Vector3.forward, Vector3.up), view.size, true); 39 | evt.Use(); 40 | break; 41 | } 42 | case KeyCode.Keypad5: 43 | { 44 | view.LookAt(view.pivot, view.rotation, view.size, !view.orthographic); 45 | evt.Use(); 46 | break; 47 | } 48 | case KeyCode.Keypad0: 49 | { 50 | if (Camera.main) 51 | { 52 | view.LookAt(view.pivot, Camera.main.transform.rotation, view.size, false); 53 | evt.Use(); 54 | } 55 | 56 | break; 57 | } 58 | } 59 | 60 | } 61 | } 62 | 63 | [Shortcut("Edit/Find In Project", KeyCode.G, ShortcutModifiers.Alt)] 64 | public static void ProjectSearch() 65 | { 66 | // Get the internal UnityEditor.ObjectBrowser window 67 | System.Type t = typeof(EditorWindow).Assembly.GetType("UnityEditor.ProjectBrowser"); 68 | 69 | // Get the window & focus it 70 | EditorWindow win = EditorWindow.GetWindow(t); 71 | win.Focus(); 72 | 73 | // Send a find command 74 | Event e = new Event(); 75 | e.type = EventType.ExecuteCommand; 76 | e.commandName = "Find"; 77 | win.SendEvent(e); 78 | } 79 | 80 | [Shortcut("Edit/Group/Create Group", KeyCode.G, ShortcutModifiers.Alt | ShortcutModifiers.Shift)] 81 | public static void CreateGroup() 82 | { 83 | GameObject newGO = new GameObject("Group"); 84 | foreach (GameObject go in Selection.gameObjects) 85 | { 86 | go.transform.parent = newGO.transform; 87 | } 88 | Selection.activeGameObject = newGO; 89 | CenterOnChildren(); 90 | } 91 | 92 | [Shortcut("Edit/Group/Center Group on Children", KeyCode.G, ShortcutModifiers.Alt)] 93 | public static void CenterOnChildren() 94 | { 95 | foreach (Transform root in Selection.GetFiltered(typeof(Transform), SelectionMode.TopLevel | SelectionMode.ExcludePrefab | SelectionMode.Editable)) 96 | { 97 | Vector3 min = new Vector3(Mathf.Infinity, Mathf.Infinity, Mathf.Infinity); 98 | Vector3 max = new Vector3(-Mathf.Infinity, -Mathf.Infinity, -Mathf.Infinity); 99 | List origPos = new List(); 100 | bool found = false; 101 | 102 | foreach (Transform t in root) 103 | { 104 | found = true; 105 | min = Vector3.Min(min, t.position); 106 | max = Vector3.Max(max, t.position); 107 | origPos.Add(t.position); 108 | } 109 | 110 | if (found) 111 | { 112 | Vector3 centerPoint = (max + min) / 2f; 113 | root.position = centerPoint; 114 | 115 | int idx = 0; 116 | foreach (Transform t in root) 117 | t.position = origPos[idx++]; 118 | } 119 | } 120 | } 121 | 122 | [MenuItem("Edit/Disable All Gizmos")] 123 | public static void DisableAllGizmos() 124 | { 125 | SceneView.lastActiveSceneView.drawGizmos = false; 126 | } 127 | 128 | [MenuItem("Edit/Enable All Gizmos")] 129 | public static void EnableAllGizmos() 130 | { 131 | SceneView.lastActiveSceneView.drawGizmos = true; 132 | } 133 | 134 | } 135 | } -------------------------------------------------------------------------------- /Editor/Edit/ReplaceSelectionWindow.cs: -------------------------------------------------------------------------------- 1 | /* This wizard will replace a selection with an object or prefab. 2 | * Scene objects will be cloned (destroying their prefab links). 3 | * Original coding by 'yesfish', nabbed from Unity Forums 4 | * 'keep parent' added by Dave A (also removed 'rotation' option, using localRotation 5 | */ 6 | using UnityEngine; 7 | using UnityEditor; 8 | using System.Collections; 9 | using System.Linq; 10 | 11 | namespace PixelWizards.Utilities 12 | { 13 | public class ReplaceSelectionWindow : EditorWindow 14 | { 15 | private static GameObject replacement = null; 16 | private static bool keepOriginals = true; 17 | private static bool keepName = false; 18 | private static bool applyChangesInChildren = false; 19 | 20 | [MenuItem("Edit/Replace Selection", false, -1)] 21 | private static void ShowWindow() 22 | { 23 | GetWindow(false, "Replace Selection", true); 24 | } 25 | 26 | private void OnGUI() 27 | { 28 | GUILayout.BeginVertical(); 29 | { 30 | GUILayout.Space(10f); 31 | 32 | GUILayout.Label("Replace Selection", EditorStyles.boldLabel); 33 | GUILayout.Label("Replace the current selection with a specific prefab", EditorStyles.helpBox); 34 | GUILayout.Space(10f); 35 | 36 | GUILayout.BeginHorizontal(); 37 | { 38 | GUILayout.Label("Replacement Object", GUILayout.Width(150f)); 39 | replacement = EditorGUILayout.ObjectField(replacement, typeof(GameObject), true, GUILayout.ExpandWidth(true)) as GameObject; 40 | } 41 | GUILayout.EndHorizontal(); 42 | 43 | GUILayout.Space(5f); 44 | 45 | keepName = EditorGUILayout.Toggle("Keep Original Names?", keepName); 46 | GUILayout.Label("Replacement objects should keep the same names as the originals", EditorStyles.helpBox); 47 | 48 | GUILayout.Space(5f); 49 | 50 | keepOriginals = EditorGUILayout.Toggle("Keep Original Objects?", keepOriginals); 51 | GUILayout.Label("Should we keep the original GameObjects or remove them?", EditorStyles.helpBox); 52 | 53 | GUILayout.Space(10f); 54 | 55 | applyChangesInChildren = EditorGUILayout.Toggle("Apply Transform Changes in Children", applyChangesInChildren); 56 | GUILayout.Label("Attempt to apply Transform overrides in child objects?", EditorStyles.helpBox); 57 | 58 | GUILayout.Space(10f); 59 | 60 | if (replacement) 61 | { 62 | if ( GUILayout.Button("Replace Selection", GUILayout.Height(35f))) 63 | { 64 | DoReplacement(); 65 | } 66 | } 67 | } 68 | GUILayout.EndVertical(); 69 | } 70 | 71 | private void DoReplacement() 72 | { 73 | // can only do replacement if we have a new object to replace with 74 | if (!replacement) 75 | return; 76 | 77 | var selectionTransforms = Selection.GetTransforms( 78 | SelectionMode.TopLevel | SelectionMode.Editable); 79 | 80 | Undo.RegisterCompleteObjectUndo(selectionTransforms, "Replace Selection"); 81 | 82 | for(var i = 0; i ().ToList(); 106 | if (newChildren.Contains(child)) 107 | { 108 | // found match in the new prefab 109 | var newChild = newChildren.FirstOrDefault(n => n.name == child.name); 110 | 111 | // did we find a child? 112 | if (!newChild) continue; 113 | 114 | newChild.position = child.position; 115 | newChild.rotation = child.rotation; 116 | newChild.localScale = child.localScale; 117 | 118 | } 119 | else 120 | { 121 | Debug.Log("Existing child " + child.name + " does not existing in new prefab?"); 122 | } 123 | } 124 | } 125 | 126 | // if we don't want to keep them then remove the originals 127 | if (keepOriginals) return; 128 | 129 | // remove the old objects 130 | foreach (var g in Selection.gameObjects) 131 | { 132 | DestroyImmediate(g); 133 | } 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /Editor/Assets/FindReference.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | using System.Text; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using UnityEngine.Events; 8 | using UnityEngine.SceneManagement; 9 | 10 | namespace PixelWizards.Utilities 11 | { 12 | public static class FindReferenceEditorTool 13 | { 14 | [MenuItem("Assets/Find all references", false, 500)] 15 | public static void FindAllReferencesToGameObject() 16 | { 17 | var go = Selection.activeGameObject; 18 | 19 | if (go == null) 20 | { 21 | Debug.LogWarning("No Object selected!"); 22 | 23 | return; 24 | } 25 | 26 | // Get all MonoBehaviours in the scene 27 | List behaviours = new List(); 28 | 29 | for (int i = 0; i < SceneManager.sceneCount; ++i) 30 | { 31 | var scene = SceneManager.GetSceneAt(i); 32 | 33 | if (!scene.isLoaded) 34 | { 35 | continue; 36 | } 37 | 38 | var roots = scene.GetRootGameObjects(); 39 | foreach (var root in roots) 40 | { 41 | behaviours.AddRange(root.GetComponentsInChildren(true)); 42 | } 43 | } 44 | 45 | var foundGameObjects = new List(); 46 | 47 | foreach (var beh in behaviours) 48 | { 49 | //Debug.Log("MonoBehaviour: " + beh); 50 | 51 | if (beh == null) 52 | { 53 | continue; 54 | } 55 | 56 | Type type = beh.GetType(); 57 | var fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance); 58 | 59 | foreach (var f in fields) 60 | { 61 | var fieldType = f.FieldType; 62 | 63 | //Debug.Log("Field: " + f + "type: " + fieldType); 64 | 65 | if (fieldType == typeof(GameObject)) 66 | { 67 | var o = f.GetValue(beh) as GameObject; 68 | 69 | Validate(o, go, beh, foundGameObjects); 70 | } 71 | else if (fieldType == typeof(GameObject[])) 72 | { 73 | var array = f.GetValue(beh) as GameObject[]; 74 | 75 | foreach (var a in array) 76 | { 77 | Validate(a, go, beh, foundGameObjects); 78 | } 79 | } 80 | else if (fieldType == typeof(Transform)) 81 | { 82 | var t = f.GetValue(beh) as Transform; 83 | 84 | if (t != null) 85 | { 86 | Validate(t.gameObject, go, beh, foundGameObjects); 87 | } 88 | } 89 | else if (f.FieldType == typeof(UnityEvent)) 90 | { 91 | var e = f.GetValue(beh) as UnityEvent; 92 | for (int i = e.GetPersistentEventCount() - 1; i >= 0; --i) 93 | { 94 | var comp = e.GetPersistentTarget(i) as Component; 95 | 96 | if (comp != null) 97 | { 98 | Validate(comp.gameObject, go, beh, foundGameObjects); 99 | } 100 | } 101 | } 102 | else if (f.FieldType == typeof(List)) 103 | { 104 | var list = f.GetValue(beh) as List; 105 | 106 | if (list != null) 107 | { 108 | foreach (var l in list) 109 | { 110 | Validate(l, go, beh, foundGameObjects); 111 | } 112 | } 113 | } 114 | else if (f.FieldType == typeof(List)) 115 | { 116 | var list = f.GetValue(beh) as List; 117 | 118 | if (list != null) 119 | { 120 | foreach (var l in list) 121 | { 122 | if (l != null) 123 | { 124 | Validate(l.gameObject, go, beh, foundGameObjects); 125 | } 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | if (foundGameObjects.Count == 0) 133 | { 134 | Debug.LogWarning("No references found to " + go); 135 | } 136 | else 137 | { 138 | EditorGUIUtility.PingObject(go); 139 | Selection.objects = foundGameObjects.ToArray(); 140 | } 141 | } 142 | 143 | private static void Validate(GameObject test, GameObject target, MonoBehaviour beh, List foundList) 144 | { 145 | if (test == target) 146 | { 147 | Debug.Log("Reference: " + beh.gameObject + "\n" + beh.gameObject.GetHierarchy()); 148 | 149 | foundList.Add(beh.gameObject); 150 | } 151 | } 152 | 153 | public static string GetHierarchy(this GameObject go) 154 | { 155 | if (go == null) 156 | { 157 | return ""; 158 | } 159 | 160 | List hierarchy = new List(6); 161 | StringBuilder sb = new StringBuilder(); 162 | Transform transform = go.transform; 163 | 164 | while (transform != null) 165 | { 166 | hierarchy.Add(transform.name); 167 | transform = transform.parent; 168 | } 169 | 170 | if (hierarchy.Count > 0) 171 | { 172 | sb.Append(hierarchy[hierarchy.Count - 1]); 173 | } 174 | 175 | for (int i = hierarchy.Count - 2; i >= 0; --i) 176 | { 177 | sb.AppendFormat(" > {0}", hierarchy[i]); 178 | } 179 | 180 | return sb.ToString(); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /Editor/Edit/GlobalDefinesWizard.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Runtime.Serialization; 5 | using System.Runtime.Serialization.Formatters.Binary; 6 | using UnityEditor; 7 | using UnityEngine; 8 | 9 | namespace PixelWizards.Utilities 10 | { 11 | public class GlobalDefinesWizard : ScriptableWizard 12 | { 13 | [System.Serializable] 14 | public class GlobalDefine : ISerializable 15 | { 16 | public string define; 17 | public bool enabled; 18 | 19 | public GlobalDefine( ) 20 | { } 21 | 22 | 23 | protected GlobalDefine(SerializationInfo info, StreamingContext context) 24 | { 25 | define = info.GetString("define"); 26 | enabled = info.GetBoolean("enabled"); 27 | } 28 | 29 | 30 | public void GetObjectData(SerializationInfo info, StreamingContext context) 31 | { 32 | info.AddValue("define", define); 33 | info.AddValue("enabled", enabled); 34 | } 35 | 36 | } 37 | 38 | private const string _prefsKey = "kGlobalDefines"; 39 | public List _globalDefines = new List(); 40 | 41 | 42 | [MenuItem("Edit/Global Defines", false, 10)] 43 | static void createWizardFromMenu( ) 44 | { 45 | var helper = ScriptableWizard.DisplayWizard("Global Defines Manager", "Save", "Cancel"); 46 | helper.minSize = new Vector2(500, 300); 47 | helper.maxSize = new Vector2(500, 300); 48 | 49 | // load up the defines 50 | if ( EditorPrefs.HasKey(_prefsKey) ) 51 | { 52 | var data = EditorPrefs.GetString(_prefsKey); 53 | var bytes = System.Convert.FromBase64String(data); 54 | var stream = new MemoryStream(bytes); 55 | 56 | var formatter = new BinaryFormatter(); 57 | helper._globalDefines = (List) formatter.Deserialize(stream); 58 | } 59 | } 60 | 61 | 62 | void OnGUI( ) 63 | { 64 | var toRemove = new List(); 65 | 66 | foreach ( var define in _globalDefines ) 67 | { 68 | if ( defineEditor(define) ) 69 | toRemove.Add(define); 70 | } 71 | 72 | foreach ( var define in toRemove ) 73 | _globalDefines.Remove(define); 74 | 75 | if ( GUILayout.Button("Add Define") ) 76 | { 77 | var d = new GlobalDefine(); 78 | d.define = "NEW_DEFINE"; 79 | d.enabled = false; 80 | _globalDefines.Add(d); 81 | } 82 | GUILayout.Space(40); 83 | 84 | if ( GUILayout.Button("Save") ) 85 | { 86 | save(); 87 | Close(); 88 | } 89 | } 90 | 91 | 92 | private void save( ) 93 | { 94 | // nothing to save means delete everything 95 | if ( _globalDefines.Count == 0 ) 96 | { 97 | deleteFiles(); 98 | 99 | EditorPrefs.DeleteKey(_prefsKey); 100 | Close(); 101 | return; 102 | } 103 | 104 | // save some stuff, first to prefs then to disk 105 | var formatter = new BinaryFormatter(); 106 | using ( var stream = new MemoryStream() ) 107 | { 108 | formatter.Serialize(stream, _globalDefines); 109 | var data = System.Convert.ToBase64String(stream.ToArray()); 110 | stream.Close(); 111 | 112 | EditorPrefs.SetString(_prefsKey, data); 113 | } 114 | 115 | // what shall we write to disk? 116 | var toDisk = _globalDefines.Where(d => d.enabled).Select(d => d.define).ToArray(); 117 | if ( toDisk.Length > 0 ) 118 | { 119 | var builder = new System.Text.StringBuilder("-define:"); 120 | for ( var i = 0; i < toDisk.Length; i++ ) 121 | { 122 | if ( i < toDisk.Length - 1 ) 123 | builder.AppendFormat("{0};", toDisk[i]); 124 | else 125 | builder.Append(toDisk[i]); 126 | } 127 | 128 | writeFiles(builder.ToString()); 129 | 130 | AssetDatabase.Refresh(); 131 | reimportSomethingToForceRecompile(); 132 | } 133 | else 134 | { 135 | // nothing enabled to save, kill the files 136 | deleteFiles(); 137 | } 138 | } 139 | 140 | 141 | private void reimportSomethingToForceRecompile( ) 142 | { 143 | var dataPathDir = new DirectoryInfo(Application.dataPath); 144 | var dataPathUri = new System.Uri(Application.dataPath); 145 | foreach ( var file in dataPathDir.GetFiles("GlobalDefinesWizard.cs", SearchOption.AllDirectories) ) 146 | { 147 | var relativeUri = dataPathUri.MakeRelativeUri(new System.Uri(file.FullName)); 148 | var relativePath = System.Uri.UnescapeDataString(relativeUri.ToString()); 149 | AssetDatabase.ImportAsset(relativePath, ImportAssetOptions.ForceUpdate); 150 | } 151 | } 152 | 153 | 154 | private void deleteFiles( ) 155 | { 156 | var smcsFile = Path.Combine(Application.dataPath, "smcs.rsp"); 157 | var gmcsFile = Path.Combine(Application.dataPath, "gmcs.rsp"); 158 | 159 | if ( File.Exists(smcsFile) ) 160 | File.Delete(smcsFile); 161 | 162 | if ( File.Exists(gmcsFile) ) 163 | File.Delete(gmcsFile); 164 | } 165 | 166 | 167 | private void writeFiles(string data) 168 | { 169 | var smcsFile = Path.Combine(Application.dataPath, "smcs.rsp"); 170 | var gmcsFile = Path.Combine(Application.dataPath, "gmcs.rsp"); 171 | 172 | // -define:debug;poop 173 | File.WriteAllText(smcsFile, data); 174 | File.WriteAllText(gmcsFile, data); 175 | } 176 | 177 | 178 | private bool defineEditor(GlobalDefine define) 179 | { 180 | EditorGUILayout.BeginHorizontal(); 181 | 182 | define.define = EditorGUILayout.TextField(define.define); 183 | define.enabled = EditorGUILayout.Toggle(define.enabled); 184 | 185 | var remove = false; 186 | if ( GUILayout.Button("Remove") ) 187 | remove = true; 188 | 189 | EditorGUILayout.EndHorizontal(); 190 | 191 | return remove; 192 | } 193 | 194 | 195 | // Called when the 'save' button is pressed 196 | void OnWizardCreate( ) 197 | { 198 | // .Net 2.0 Subset: smcs.rsp 199 | // .Net 2.0: gmcs.rsp 200 | // -define:debug;poop 201 | } 202 | 203 | 204 | void OnWizardOtherButton( ) 205 | { 206 | this.Close(); 207 | } 208 | 209 | } 210 | } -------------------------------------------------------------------------------- /Editor/Edit/TextToTextMeshPro.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using UnityEngine.UI; 4 | using TMPro; 5 | using System; 6 | 7 | namespace PixelWizards.Utilities 8 | { 9 | public class TextToTextMeshPro : Editor 10 | { 11 | public class TextMeshProSettings 12 | { 13 | public bool Enabled; 14 | public FontStyles FontStyle; 15 | public float FontSize; 16 | public float FontSizeMin; 17 | public float FontSizeMax; 18 | public float LineSpacing; 19 | public bool EnableRichText; 20 | public bool EnableAutoSizing; 21 | public TextAlignmentOptions TextAlignmentOptions; 22 | public TextOverflowModes TextOverflowModes; 23 | public string Text; 24 | public Color Color; 25 | public bool RayCastTarget; 26 | } 27 | 28 | [MenuItem("Edit/Text To TextMeshPro", false, 4000)] 29 | private static void DoIt() 30 | { 31 | if (TMP_Settings.defaultFontAsset == null) 32 | { 33 | EditorUtility.DisplayDialog("ERROR!", "Assign a default font asset in project settings!", "OK", ""); 34 | return; 35 | } 36 | 37 | foreach (var parent in Selection.gameObjects) 38 | { 39 | ConvertChildren(parent); 40 | } 41 | } 42 | 43 | /// 44 | /// Recursive function to convert all of the children of the initial object that is passed in 45 | /// 46 | /// 47 | private static void ConvertChildren(GameObject parent) 48 | { 49 | // iterate through all of the children of the selection and convert them 50 | Debug.Log("Converting: " + parent.name); 51 | var childCount = parent.transform.childCount; 52 | for (var i = 0; i < childCount; i++) 53 | { 54 | var child = parent.transform.GetChild(i).gameObject; 55 | Debug.Log("Converting: " + child.name); 56 | ConvertTextToTextMeshPro(child); 57 | ConvertChildren(child); 58 | } 59 | } 60 | 61 | private static void ConvertTextToTextMeshPro(GameObject target) 62 | { 63 | var settings = GetTextMeshProSettings(target); 64 | if (settings == null) 65 | { 66 | // no text component on this one, ignore 67 | return; 68 | } 69 | 70 | try 71 | { 72 | DestroyImmediate(target.GetComponent()); 73 | } 74 | catch(Exception e) 75 | { 76 | Debug.Log("Could not delete Text component from object " + target.name + " : Message: " + e.Message); 77 | return; 78 | } 79 | 80 | var tmp = target.AddComponent(); 81 | tmp.enabled = settings.Enabled; 82 | tmp.fontStyle = settings.FontStyle; 83 | tmp.fontSize = settings.FontSize; 84 | tmp.fontSizeMin = settings.FontSizeMin; 85 | tmp.fontSizeMax = settings.FontSizeMax; 86 | tmp.lineSpacing = settings.LineSpacing; 87 | tmp.richText = settings.EnableRichText; 88 | tmp.enableAutoSizing = settings.EnableAutoSizing; 89 | tmp.alignment = settings.TextAlignmentOptions; 90 | tmp.overflowMode = settings.TextOverflowModes; 91 | tmp.text = settings.Text; 92 | tmp.color = settings.Color; 93 | tmp.raycastTarget = settings.RayCastTarget; 94 | } 95 | 96 | private static TextMeshProSettings GetTextMeshProSettings(GameObject gameObject) 97 | { 98 | var uiText = gameObject.GetComponent(); 99 | if (uiText == null) 100 | { 101 | return null; 102 | } 103 | 104 | return new TextMeshProSettings 105 | { 106 | Enabled = uiText.enabled, 107 | FontStyle = FontStyleToFontStyles(uiText.fontStyle), 108 | FontSize = uiText.fontSize, 109 | FontSizeMin = uiText.resizeTextMinSize, 110 | FontSizeMax = uiText.resizeTextMaxSize, 111 | LineSpacing = uiText.lineSpacing, 112 | EnableRichText = uiText.supportRichText, 113 | EnableAutoSizing = uiText.resizeTextForBestFit, 114 | TextAlignmentOptions = TextAnchorToTextAlignmentOptions(uiText.alignment), 115 | TextOverflowModes = VerticalWrapModeToTextOverflowModes(uiText.verticalOverflow), 116 | Text = uiText.text, 117 | Color = uiText.color, 118 | RayCastTarget = uiText.raycastTarget 119 | }; 120 | } 121 | 122 | static bool HorizontalWrapModeToBool(HorizontalWrapMode overflow) 123 | { 124 | return overflow == HorizontalWrapMode.Wrap; 125 | } 126 | 127 | static TextOverflowModes VerticalWrapModeToTextOverflowModes(VerticalWrapMode verticalOverflow) 128 | { 129 | return verticalOverflow == VerticalWrapMode.Truncate ? TextOverflowModes.Truncate : TextOverflowModes.Overflow; 130 | } 131 | 132 | static FontStyles FontStyleToFontStyles(FontStyle fontStyle) 133 | { 134 | switch (fontStyle) 135 | { 136 | case FontStyle.Normal: 137 | return FontStyles.Normal; 138 | 139 | case FontStyle.Bold: 140 | return FontStyles.Bold; 141 | 142 | case FontStyle.Italic: 143 | return FontStyles.Italic; 144 | 145 | case FontStyle.BoldAndItalic: 146 | return FontStyles.Bold | FontStyles.Italic; 147 | } 148 | 149 | Debug.LogWarning("Unhandled font style " + fontStyle); 150 | return FontStyles.Normal; 151 | } 152 | 153 | static TextAlignmentOptions TextAnchorToTextAlignmentOptions(TextAnchor textAnchor) 154 | { 155 | switch (textAnchor) 156 | { 157 | case TextAnchor.UpperLeft: 158 | return TextAlignmentOptions.TopLeft; 159 | 160 | case TextAnchor.UpperCenter: 161 | return TextAlignmentOptions.Top; 162 | 163 | case TextAnchor.UpperRight: 164 | return TextAlignmentOptions.TopRight; 165 | 166 | case TextAnchor.MiddleLeft: 167 | return TextAlignmentOptions.Left; 168 | 169 | case TextAnchor.MiddleCenter: 170 | return TextAlignmentOptions.Center; 171 | 172 | case TextAnchor.MiddleRight: 173 | return TextAlignmentOptions.Right; 174 | 175 | case TextAnchor.LowerLeft: 176 | return TextAlignmentOptions.BottomLeft; 177 | 178 | case TextAnchor.LowerCenter: 179 | return TextAlignmentOptions.Bottom; 180 | 181 | case TextAnchor.LowerRight: 182 | return TextAlignmentOptions.BottomRight; 183 | } 184 | 185 | Debug.LogWarning("Unhandled text anchor " + textAnchor); 186 | return TextAlignmentOptions.TopLeft; 187 | } 188 | } 189 | } -------------------------------------------------------------------------------- /Editor/Assets/Cleanup/UnityGuidRegenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using UnityEditor; 5 | using UnityEngine; 6 | 7 | namespace PixelWizards.Utilities 8 | { 9 | public class UnityGuidRegeneratorMenu 10 | { 11 | [MenuItem("Assets/Cleanup/Regenerate asset GUIDs", false, 1000)] 12 | public static void RegenerateGuids() 13 | { 14 | if (EditorUtility.DisplayDialog("GUIDs regeneration", 15 | "You are going to start the process of GUID regeneration. \n\nDO NOT DO THIS IF YOU DON'T KNOW WHAT THIS MEANS. \n\nIt may have unexpected results. \n\n MAKE A PROJECT BACKUP BEFORE PROCEEDING!", 16 | "Regenerate GUIDs", "Cancel")) 17 | { 18 | try 19 | { 20 | Debug.Log("****** STARTING GUID REGENERATION ******"); 21 | AssetDatabase.StartAssetEditing(); 22 | 23 | string path = Path.GetFullPath(".") + Path.DirectorySeparatorChar + "Assets"; 24 | UnityGuidRegenerator regenerator = new UnityGuidRegenerator(path); 25 | regenerator.RegenerateGuids(); 26 | Debug.Log("****** GUID REGENERATION COMPLETE ******"); 27 | } 28 | finally 29 | { 30 | AssetDatabase.StopAssetEditing(); 31 | EditorUtility.ClearProgressBar(); 32 | AssetDatabase.Refresh(); 33 | } 34 | } 35 | } 36 | } 37 | 38 | internal class UnityGuidRegenerator 39 | { 40 | private static readonly string[] kDefaultFileExtensions = { 41 | "*.meta", 42 | "*.mat", 43 | "*.anim", 44 | "*.prefab", 45 | "*.unity", 46 | "*.asset" 47 | }; 48 | 49 | private readonly string _assetsPath; 50 | 51 | public UnityGuidRegenerator(string assetsPath) 52 | { 53 | _assetsPath = assetsPath; 54 | } 55 | 56 | public void RegenerateGuids(string[] regeneratedExtensions = null) 57 | { 58 | if (regeneratedExtensions == null) 59 | { 60 | regeneratedExtensions = kDefaultFileExtensions; 61 | } 62 | 63 | // Get list of working files 64 | List filesPaths = new List(); 65 | foreach (string extension in regeneratedExtensions) 66 | { 67 | filesPaths.AddRange( 68 | Directory.GetFiles(_assetsPath, extension, SearchOption.AllDirectories) 69 | ); 70 | } 71 | 72 | // Create dictionary to hold old-to-new GUID map 73 | Dictionary guidOldToNewMap = new Dictionary(); 74 | Dictionary> guidsInFileMap = new Dictionary>(); 75 | 76 | // We must only replace GUIDs for Resources present in Assets. 77 | // Otherwise built-in resources (shader, meshes etc) get overwritten. 78 | HashSet ownGuids = new HashSet(); 79 | 80 | // Traverse all files, remember which GUIDs are in which files and generate new GUIDs 81 | int counter = 0; 82 | foreach (string filePath in filesPaths) 83 | { 84 | EditorUtility.DisplayProgressBar("Scanning Assets folder", MakeRelativePath(_assetsPath, filePath), counter / (float)filesPaths.Count); 85 | string contents = File.ReadAllText(filePath); 86 | 87 | IEnumerable guids = GetGuids(contents); 88 | bool isFirstGuid = true; 89 | foreach (string oldGuid in guids) 90 | { 91 | // First GUID in .meta file is always the GUID of the asset itself 92 | if (isFirstGuid && Path.GetExtension(filePath) == ".meta") 93 | { 94 | ownGuids.Add(oldGuid); 95 | isFirstGuid = false; 96 | } 97 | // Generate and save new GUID if we haven't added it before 98 | if (!guidOldToNewMap.ContainsKey(oldGuid)) 99 | { 100 | string newGuid = Guid.NewGuid().ToString("N"); 101 | guidOldToNewMap.Add(oldGuid, newGuid); 102 | } 103 | 104 | if (!guidsInFileMap.ContainsKey(filePath)) 105 | guidsInFileMap[filePath] = new List(); 106 | 107 | if (!guidsInFileMap[filePath].Contains(oldGuid)) 108 | { 109 | guidsInFileMap[filePath].Add(oldGuid); 110 | } 111 | } 112 | 113 | counter++; 114 | } 115 | 116 | // Traverse the files again and replace the old GUIDs 117 | counter = -1; 118 | int guidsInFileMapKeysCount = guidsInFileMap.Keys.Count; 119 | foreach (string filePath in guidsInFileMap.Keys) 120 | { 121 | EditorUtility.DisplayProgressBar("Regenerating GUIDs", MakeRelativePath(_assetsPath, filePath), counter / (float)guidsInFileMapKeysCount); 122 | counter++; 123 | 124 | string contents = File.ReadAllText(filePath); 125 | foreach (string oldGuid in guidsInFileMap[filePath]) 126 | { 127 | if (!ownGuids.Contains(oldGuid)) 128 | continue; 129 | 130 | string newGuid = guidOldToNewMap[oldGuid]; 131 | if (string.IsNullOrEmpty(newGuid)) 132 | throw new NullReferenceException("newGuid == null"); 133 | 134 | contents = contents.Replace("guid: " + oldGuid, "guid: " + newGuid); 135 | } 136 | File.WriteAllText(filePath, contents); 137 | } 138 | 139 | Debug.Log("Regenerated: " + guidsInFileMap.Count + " GUIDS..."); 140 | 141 | EditorUtility.ClearProgressBar(); 142 | } 143 | 144 | private static IEnumerable GetGuids(string text) 145 | { 146 | const string guidStart = "guid: "; 147 | const int guidLength = 32; 148 | int textLength = text.Length; 149 | int guidStartLength = guidStart.Length; 150 | List guids = new List(); 151 | 152 | int index = 0; 153 | while (index + guidStartLength + guidLength < textLength) 154 | { 155 | index = text.IndexOf(guidStart, index, StringComparison.Ordinal); 156 | if (index == -1) 157 | break; 158 | 159 | index += guidStartLength; 160 | string guid = text.Substring(index, guidLength); 161 | index += guidLength; 162 | 163 | if (IsGuid(guid)) 164 | { 165 | guids.Add(guid); 166 | } 167 | } 168 | 169 | return guids; 170 | } 171 | 172 | private static bool IsGuid(string text) 173 | { 174 | for (int i = 0; i < text.Length; i++) 175 | { 176 | char c = text[i]; 177 | if ( 178 | !((c >= '0' && c <= '9') || 179 | (c >= 'a' && c <= 'z')) 180 | ) 181 | return false; 182 | } 183 | 184 | return true; 185 | } 186 | 187 | private static string MakeRelativePath(string fromPath, string toPath) 188 | { 189 | Uri fromUri = new Uri(fromPath); 190 | Uri toUri = new Uri(toPath); 191 | 192 | Uri relativeUri = fromUri.MakeRelativeUri(toUri); 193 | string relativePath = Uri.UnescapeDataString(relativeUri.ToString()); 194 | 195 | return relativePath; 196 | } 197 | } 198 | } -------------------------------------------------------------------------------- /Editor/Assets/BatchImportAssetPackagesWindow.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using UnityEditor; 7 | using UnityEngine; 8 | using Unity.EditorCoroutines.Editor; 9 | using Debug = UnityEngine.Debug; 10 | 11 | namespace PixelWizards.Utilities 12 | { 13 | public class BatchImportAssetPackagesWindow : EditorWindow 14 | { 15 | 16 | private string packagePath = ""; 17 | private bool includeSubdirectories = false; 18 | private bool enableLogging = true; 19 | 20 | private Queue importQueue = new Queue(); 21 | private List previewPackages = new List(); 22 | private Vector2 previewScrollPos; 23 | 24 | private bool isImporting = false; 25 | private bool isPaused = false; 26 | private float timePerMB = 0.1f; // Seconds per MB for estimated duration 27 | 28 | private const string EditorPrefsKey_PackagePath = "BatchImportAssetPackages_LastPath"; 29 | private const string EditorPrefsKey_RecentFolders = "BatchImportAssetPackages_RecentFolders"; 30 | private const string EditorPrefsKey_IncludeSubdirectories = "BatchImportAssetPackages_IncludeSubdirectories"; 31 | 32 | private List recentFolders = new List(); 33 | private const int MaxRecentFolders = 5; 34 | 35 | private EditorCoroutine importCoroutine; 36 | 37 | [MenuItem("Assets/Batch Import Packages")] 38 | public static void ShowWindow() 39 | { 40 | var window = GetWindow("Batch Import Packages"); 41 | window.minSize = new Vector2(500, 320); 42 | } 43 | 44 | private void OnEnable() 45 | { 46 | if (EditorPrefs.HasKey(EditorPrefsKey_PackagePath)) 47 | { 48 | packagePath = EditorPrefs.GetString(EditorPrefsKey_PackagePath); 49 | } 50 | 51 | if (EditorPrefs.HasKey(EditorPrefsKey_RecentFolders)) 52 | { 53 | string saved = EditorPrefs.GetString(EditorPrefsKey_RecentFolders); 54 | recentFolders = saved.Split('|').Distinct().Where(Directory.Exists).ToList(); 55 | } 56 | 57 | includeSubdirectories = EditorPrefs.GetBool(EditorPrefsKey_IncludeSubdirectories, false); 58 | } 59 | 60 | private void OnDisable() 61 | { 62 | if (!string.IsNullOrEmpty(packagePath)) 63 | EditorPrefs.SetString(EditorPrefsKey_PackagePath, packagePath); 64 | } 65 | 66 | private void OnGUI() 67 | { 68 | GUILayout.Label("Batch Import .unitypackage Files", EditorStyles.boldLabel); 69 | EditorGUILayout.Space(); 70 | 71 | if (recentFolders.Count > 0) 72 | { 73 | GUILayout.BeginHorizontal(); 74 | { 75 | 76 | GUILayout.Label("Recent Folders:", GUILayout.Width(100)); 77 | 78 | // Dropdown for selecting recent folder 79 | int selectedIndex = -1; 80 | string[] folderNames = recentFolders.Select(f => Path.GetFileName(f)).ToArray(); 81 | selectedIndex = EditorGUILayout.Popup(-1, folderNames); 82 | 83 | if (selectedIndex >= 0 && selectedIndex < recentFolders.Count) 84 | { 85 | packagePath = recentFolders[selectedIndex]; 86 | ScanForPackages(); 87 | GUI.FocusControl(null); 88 | } 89 | 90 | // "Clear" button 91 | if (GUILayout.Button("Clear", GUILayout.Width(60))) 92 | { 93 | if (EditorUtility.DisplayDialog("Clear Recent Folders", 94 | "Are you sure you want to clear the recent folders list?", "Yes", "Cancel")) 95 | { 96 | recentFolders.Clear(); 97 | EditorPrefs.DeleteKey(EditorPrefsKey_RecentFolders); 98 | } 99 | } 100 | } 101 | GUILayout.EndHorizontal(); 102 | } 103 | EditorGUILayout.LabelField("Package Folder Path", EditorStyles.label); 104 | GUILayout.BeginHorizontal(); 105 | { 106 | packagePath = EditorGUILayout.TextField(packagePath); 107 | if (GUILayout.Button("Browse", GUILayout.Width(70))) 108 | { 109 | string selectedPath = 110 | EditorUtility.OpenFolderPanel("Select Folder Containing Packages", packagePath, ""); 111 | if (!string.IsNullOrEmpty(selectedPath)) 112 | { 113 | packagePath = selectedPath; 114 | ScanForPackages(); 115 | GUI.FocusControl(null); 116 | } 117 | } 118 | } 119 | EditorGUILayout.EndHorizontal(); 120 | 121 | bool newIncludeSubdirectories = EditorGUILayout.Toggle("Include Subdirectories", includeSubdirectories); 122 | if (newIncludeSubdirectories != includeSubdirectories) 123 | { 124 | includeSubdirectories = newIncludeSubdirectories; 125 | EditorPrefs.SetBool(EditorPrefsKey_IncludeSubdirectories, includeSubdirectories); 126 | } 127 | 128 | enableLogging = EditorGUILayout.Toggle("Enable Import Logging", enableLogging); 129 | EditorGUILayout.Space(); 130 | 131 | // Package Preview List 132 | if (previewPackages.Count > 0) 133 | { 134 | GUILayout.Label($"Found {previewPackages.Count} package(s):", EditorStyles.miniBoldLabel); 135 | 136 | float totalSizeMB = previewPackages.Sum(f => f.Length) / (1024f * 1024f); 137 | float estimatedTime = totalSizeMB * timePerMB; 138 | 139 | EditorGUILayout.HelpBox($"Estimated Total Size: {totalSizeMB:F2} MB\nEstimated Time: {estimatedTime:F1} seconds", MessageType.Info); 140 | 141 | previewScrollPos = EditorGUILayout.BeginScrollView(previewScrollPos, GUILayout.Height(150)); 142 | for (int i = previewPackages.Count - 1; i >= 0; i--) 143 | { 144 | var file = previewPackages[i]; 145 | GUILayout.BeginHorizontal(); 146 | { 147 | string name = Path.GetFileName(file.FullName); 148 | float sizeMB = file.Length / (1024f * 1024f); 149 | GUILayout.Label($"{name} ({sizeMB:F2} MB)", GUILayout.MaxWidth(position.width - 180)); 150 | 151 | if (!isImporting && GUILayout.Button("Import This", GUILayout.Width(80))) 152 | { 153 | ImportSinglePackage(file.FullName); 154 | } 155 | 156 | if (!isImporting && GUILayout.Button("🗑", GUILayout.Width(25))) 157 | { 158 | if (EditorUtility.DisplayDialog("Remove Package", $"Remove {name} from the list?", "Yes", "Cancel")) 159 | { 160 | previewPackages.RemoveAt(i); 161 | 162 | // Also remove from the queue if it was already enqueued 163 | string normalizedPath = file.FullName.Replace("\\", "/"); 164 | importQueue = new Queue(importQueue.Where(p => p != normalizedPath)); 165 | 166 | } 167 | } 168 | } 169 | GUILayout.EndHorizontal(); 170 | } 171 | 172 | EditorGUILayout.EndScrollView(); 173 | 174 | EditorGUILayout.Space(); 175 | if (previewPackages.Count == 0) 176 | { 177 | EditorGUILayout.HelpBox("No .unitypackage files found. Try enabling 'Include Subdirectories'.", MessageType.Info); 178 | } 179 | if (isImporting) 180 | { 181 | EditorGUILayout.HelpBox($"Remaining in queue: {importQueue.Count}", MessageType.None); 182 | } 183 | } 184 | 185 | // Action Buttons 186 | if (!isImporting) 187 | { 188 | GUI.enabled = previewPackages.Count > 0; 189 | if (GUILayout.Button("Import Packages from Folder", GUILayout.Height(35))) 190 | { 191 | EnqueuePackagesAndStart(); 192 | } 193 | GUI.enabled = true; 194 | } 195 | 196 | if (isImporting) 197 | { 198 | EditorGUILayout.BeginHorizontal(); 199 | 200 | if (GUILayout.Button(isPaused ? "Resume" : "Pause", GUILayout.Height(35))) 201 | { 202 | isPaused = !isPaused; 203 | } 204 | 205 | if (GUILayout.Button("Cancel", GUILayout.Height(35))) 206 | { 207 | CancelImport(); 208 | } 209 | 210 | if (GUILayout.Button("Clear Queue", GUILayout.Height(35))) 211 | { 212 | ClearQueue(); 213 | } 214 | 215 | EditorGUILayout.EndHorizontal(); 216 | } 217 | } 218 | 219 | private void ScanForPackages() 220 | { 221 | previewPackages.Clear(); 222 | 223 | if (string.IsNullOrEmpty(packagePath) || !Directory.Exists(packagePath)) 224 | { 225 | Debug.LogWarning($"Invalid path: {packagePath}"); 226 | return; 227 | } 228 | 229 | var foundFiles = Directory.GetFiles(packagePath, "*.unitypackage", 230 | includeSubdirectories ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly); 231 | 232 | if (foundFiles.Length == 0 && !includeSubdirectories) 233 | { 234 | if (EditorUtility.DisplayDialog( 235 | "No packages found", 236 | "No .unitypackage files were found in the selected folder.\n\nWould you like to include subdirectories and try again?", 237 | "Yes", "No")) 238 | { 239 | includeSubdirectories = true; 240 | Repaint(); // 🔁 Immediately update the UI toggle 241 | EditorPrefs.SetBool(EditorPrefsKey_IncludeSubdirectories, true); 242 | ScanForPackages(); 243 | return; 244 | } 245 | } 246 | 247 | Debug.Log($"[BatchImport] Found {foundFiles.Length} unitypackage files in {packagePath}"); 248 | 249 | foreach (var path in foundFiles) 250 | { 251 | previewPackages.Add(new FileInfo(path)); 252 | } 253 | 254 | // Save recent folder 255 | if (!recentFolders.Contains(packagePath)) 256 | { 257 | recentFolders.Insert(0, packagePath); 258 | if (recentFolders.Count > MaxRecentFolders) 259 | recentFolders = recentFolders.Take(MaxRecentFolders).ToList(); 260 | 261 | string save = string.Join("|", recentFolders); 262 | EditorPrefs.SetString(EditorPrefsKey_RecentFolders, save); 263 | } 264 | } 265 | 266 | private void ImportSinglePackage(string path) 267 | { 268 | if (importCoroutine != null) 269 | EditorCoroutineUtility.StopCoroutine(importCoroutine); 270 | 271 | importQueue.Clear(); 272 | importQueue.Enqueue(path.Replace("\\", "/")); 273 | 274 | isImporting = true; 275 | isPaused = false; 276 | 277 | importCoroutine = EditorCoroutineUtility.StartCoroutine(ImportPackagesCoroutine(), this); 278 | } 279 | 280 | 281 | private void EnqueuePackagesAndStart() 282 | { 283 | importQueue.Clear(); 284 | foreach (var file in previewPackages) 285 | { 286 | importQueue.Enqueue(file.FullName.Replace("\\", "/")); 287 | } 288 | 289 | if (importQueue.Count > 0) 290 | { 291 | EditorPrefs.SetString(EditorPrefsKey_PackagePath, packagePath); 292 | StartImportQueue(); 293 | } 294 | } 295 | 296 | private void ClearQueue() 297 | { 298 | if (importCoroutine != null) 299 | EditorCoroutineUtility.StopCoroutine(importCoroutine); 300 | 301 | importQueue.Clear(); 302 | importCoroutine = null; 303 | isImporting = false; 304 | isPaused = false; 305 | 306 | EditorUtility.ClearProgressBar(); 307 | Debug.Log("🧹 Import queue cleared."); 308 | } 309 | 310 | private void StartImportQueue() 311 | { 312 | if (importCoroutine != null) 313 | EditorCoroutineUtility.StopCoroutine(importCoroutine); 314 | 315 | isImporting = true; 316 | isPaused = false; 317 | 318 | importCoroutine = EditorCoroutineUtility.StartCoroutine(ImportPackagesCoroutine(), this); 319 | } 320 | 321 | private void CancelImport() 322 | { 323 | if (importCoroutine != null) 324 | EditorCoroutineUtility.StopCoroutine(importCoroutine); 325 | 326 | importQueue.Clear(); 327 | importCoroutine = null; 328 | isImporting = false; 329 | isPaused = false; 330 | 331 | EditorUtility.ClearProgressBar(); 332 | Debug.LogWarning("Import cancelled by user."); 333 | } 334 | 335 | private IEnumerator ImportPackagesCoroutine() 336 | { 337 | int total = previewPackages.Count; 338 | 339 | while (importQueue.Count > 0) 340 | { 341 | while (isPaused) yield return null; 342 | 343 | string currentPath = importQueue.Dequeue(); 344 | string fileName = Path.GetFileName(currentPath); 345 | float sizeMB = new FileInfo(currentPath).Length / (1024f * 1024f); 346 | float simulatedDuration = sizeMB * timePerMB; 347 | 348 | float progress = 1f - (importQueue.Count / (float)total); 349 | EditorUtility.DisplayProgressBar("Importing Packages", $"Importing {fileName} ({sizeMB:F2} MB)...", progress); 350 | 351 | var start = Time.realtimeSinceStartup; 352 | 353 | try 354 | { 355 | AssetDatabase.ImportPackage(currentPath, false); 356 | } 357 | catch (Exception ex) 358 | { 359 | Debug.LogError($"❌ Failed to import {fileName}: {ex.Message}"); 360 | } 361 | 362 | var end = Time.realtimeSinceStartup; 363 | float actualDuration = end - start; 364 | 365 | if (enableLogging) 366 | { 367 | Debug.Log($"✔ Imported: {fileName} ({sizeMB:F2} MB) in {actualDuration:F2}s"); 368 | } 369 | 370 | yield return new EditorWaitForSeconds(Mathf.Max(simulatedDuration, 0.1f)); 371 | } 372 | 373 | EditorUtility.ClearProgressBar(); 374 | Debug.Log("✅ All packages imported."); 375 | isImporting = false; 376 | importCoroutine = null; 377 | } 378 | } 379 | } 380 | -------------------------------------------------------------------------------- /Editor/Assets/TextureCombiner.cs: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | by Jean Moreno 4 | from https://gist.github.com/jean-moreno/724c5d04d619c55f0bcda433b053df5d 5 | 6 | */ 7 | using UnityEngine; 8 | using UnityEditor; 9 | using System.Collections.Generic; 10 | 11 | namespace PixelWizards.Utilities 12 | { 13 | public class TextureCombiner : EditorWindow 14 | { 15 | [MenuItem("Assets/Texture Combiner...")] 16 | static void Open() 17 | { 18 | var w = GetWindow(true, "Texture Combiner"); 19 | w.minSize = new Vector2(870, 420); 20 | w.maxSize = new Vector2(870, 420); 21 | } 22 | 23 | public enum SaveFormat { PNG, EXR } 24 | public enum Channel { R, G, B, A, RGBLuminance } 25 | 26 | Texture2D textureR, textureG, textureB, textureA; 27 | Channel sourceR, sourceG, sourceB, sourceA; 28 | RenderTexture textureCombined; 29 | RenderTexture textureCombinedAlpha; 30 | Material blitMaterial; 31 | Material blitMaterialAlpha; 32 | SaveFormat saveFormat; 33 | bool removeCompression = true; 34 | bool removeCompressionPreview = false; 35 | int textureSize; 36 | string[] textureSizes = new string[] { "128", "256", "512", "1024", "2048", "4096", "Custom" }; 37 | int textureWidth = 128; 38 | int textureHeight = 128; 39 | Texture2D textureSaved; 40 | 41 | void OnGUI() 42 | { 43 | GUILayout.Label("TEXTURE COMBINER", EditorStyles.boldLabel); 44 | GUILayout.Space(8); 45 | 46 | EditorGUI.BeginChangeCheck(); 47 | GUILayout.BeginHorizontal(); 48 | GUILayout.Label("Combined texture: ", GUILayout.ExpandWidth(false)); 49 | var newTexture = (Texture2D)EditorGUILayout.ObjectField(textureSaved, typeof(Texture2D), false); 50 | GUILayout.EndHorizontal(); 51 | if (EditorGUI.EndChangeCheck() && newTexture != textureSaved) 52 | { 53 | if (Load(newTexture)) 54 | { 55 | textureSaved = newTexture; 56 | } 57 | } 58 | GUILayout.Space(8f); 59 | 60 | float space = 8f; 61 | var r = EditorGUILayout.GetControlRect(false, (64f + space) * 4f); 62 | var texRect = r; 63 | texRect.x += 24f; 64 | texRect.height = 64f; 65 | texRect.width = 64f; 66 | textureR = (Texture2D)EditorGUI.ObjectField(texRect, textureR, typeof(Texture2D), false); 67 | texRect.y += 64f + space; 68 | textureG = (Texture2D)EditorGUI.ObjectField(texRect, textureG, typeof(Texture2D), false); 69 | texRect.y += 64f + space; 70 | textureB = (Texture2D)EditorGUI.ObjectField(texRect, textureB, typeof(Texture2D), false); 71 | texRect.y += 64f + space; 72 | textureA = (Texture2D)EditorGUI.ObjectField(texRect, textureA, typeof(Texture2D), false); 73 | 74 | var lblRect = r; 75 | lblRect.width = 20f; 76 | lblRect.x += 4f; 77 | lblRect.y += 22f; 78 | GUI.Label(lblRect, "R", EditorStyles.largeLabel); 79 | lblRect.y += 64f + space; 80 | GUI.Label(lblRect, "G", EditorStyles.largeLabel); 81 | lblRect.y += 64f + space; 82 | GUI.Label(lblRect, "B", EditorStyles.largeLabel); 83 | lblRect.y += 64f + space; 84 | GUI.Label(lblRect, "A", EditorStyles.largeLabel); 85 | 86 | var chanRect = r; 87 | chanRect.width = 120f; 88 | chanRect.x += texRect.x + texRect.width + space; 89 | chanRect.height = 64f; 90 | sourceR = (Channel)GUI_SourceChannel(chanRect, sourceR); 91 | chanRect.y += 64f + space; 92 | sourceG = (Channel)GUI_SourceChannel(chanRect, sourceG); 93 | chanRect.y += 64f + space; 94 | sourceB = (Channel)GUI_SourceChannel(chanRect, sourceB); 95 | chanRect.y += 64f + space; 96 | sourceA = (Channel)GUI_SourceChannel(chanRect, sourceA); 97 | 98 | var resultRect = r; 99 | resultRect.height = (64f + space) * 4f; 100 | resultRect.x += lblRect.x + lblRect.width + texRect.width + chanRect.width + 64f; 101 | resultRect.width = resultRect.height; 102 | 103 | if (textureCombined != null || textureSaved != null) 104 | { 105 | var alphaRect = resultRect; 106 | alphaRect.x += resultRect.width + space; 107 | 108 | //handy way to highlight the saved texture when clicking on the big preview 109 | if (textureSaved != null) 110 | { 111 | EditorGUI.ObjectField(resultRect, textureSaved, typeof(Texture2D), false); 112 | EditorGUI.ObjectField(alphaRect, textureSaved, typeof(Texture2D), false); 113 | } 114 | 115 | if (textureCombined != null) 116 | { 117 | GUI.Box(resultRect, GUIContent.none); 118 | GUI.Box(alphaRect, GUIContent.none); 119 | //rgb 120 | GUI.DrawTexture(resultRect, textureCombined, ScaleMode.StretchToFill, false, 0); 121 | //alpha 122 | GUI.DrawTexture(alphaRect, textureCombinedAlpha, ScaleMode.StretchToFill, false, 0); 123 | } 124 | } 125 | else 126 | { 127 | resultRect.width += resultRect.width + space; 128 | EditorGUI.HelpBox(resultRect, "texture not generated yet", MessageType.Warning); 129 | } 130 | 131 | GUILayout.Space(8f); 132 | GUILayout.BeginHorizontal(); 133 | 134 | //Texture size 135 | EditorGUI.BeginChangeCheck(); 136 | textureSize = EditorGUILayout.Popup(textureSize, textureSizes, GUILayout.Width(60f)); 137 | using (new EditorGUI.DisabledScope(textureSize != textureSizes.Length - 1)) 138 | textureWidth = EditorGUILayout.IntField(textureWidth, GUILayout.Width(60f)); 139 | GUILayout.Label("x"); 140 | using (new EditorGUI.DisabledScope(textureSize != textureSizes.Length - 1)) 141 | textureHeight = EditorGUILayout.IntField(textureHeight, GUILayout.Width(60f)); 142 | if (EditorGUI.EndChangeCheck()) 143 | { 144 | textureWidth = Mathf.Clamp(textureWidth, 1, 16384); 145 | textureHeight = Mathf.Clamp(textureHeight, 1, 16384); 146 | TextureSizeUpdated(); 147 | } 148 | 149 | GUILayout.FlexibleSpace(); 150 | 151 | //Save button 152 | if (GUILayout.Button("SAVE AS...", GUILayout.Width(120f))) 153 | { 154 | SaveAs(saveFormat); 155 | } 156 | GUILayout.EndHorizontal(); 157 | 158 | //Options 159 | GUILayout.BeginHorizontal(); 160 | saveFormat = (SaveFormat)EditorGUILayout.EnumPopup(saveFormat, GUILayout.Width(60f)); 161 | removeCompression = GUILayout.Toggle(removeCompression, new GUIContent("Remove compression (saved texture)", "Remove compression from input textures for the saved texture"), EditorStyles.miniButton); 162 | removeCompressionPreview = GUILayout.Toggle(removeCompressionPreview, new GUIContent("Remove compression (preview)", "Remove compression from input textures for the preview image.\n\nThis is a separate setting because disabling/enabling back compression takes a few seconds and that can be annoying when regularly changing the inputs."), EditorStyles.miniButton); 163 | 164 | GUILayout.FlexibleSpace(); 165 | 166 | //Reset button 167 | if (GUILayout.Button("RESET", GUILayout.Width(120f))) 168 | { 169 | Reset(); 170 | } 171 | GUILayout.EndHorizontal(); 172 | 173 | if (GUI.changed) 174 | { 175 | RefreshCombinedTexture(true); 176 | } 177 | } 178 | 179 | void TextureSizeUpdated() 180 | { 181 | if (textureSize != textureSizes.Length - 1) 182 | { 183 | textureWidth = int.Parse(textureSizes[textureSize]); 184 | textureHeight = textureWidth; 185 | } 186 | UpdateRenderTextures(true); 187 | } 188 | 189 | void Reset() 190 | { 191 | OnDestroy(); 192 | sourceR = Channel.R; 193 | sourceG = Channel.R; 194 | sourceB = Channel.R; 195 | sourceA = Channel.R; 196 | textureR = null; 197 | textureG = null; 198 | textureB = null; 199 | textureA = null; 200 | textureSaved = null; 201 | } 202 | 203 | void UpdateRenderTextures(bool delete) 204 | { 205 | if (delete && textureCombined != null) 206 | ClearRenderTexture(textureCombined); 207 | if (delete && textureCombinedAlpha != null) 208 | ClearRenderTexture(textureCombinedAlpha); 209 | 210 | if (textureCombined == null) 211 | { 212 | textureCombined = new RenderTexture(textureWidth, textureHeight, 0, RenderTextureFormat.ARGB32, RenderTextureReadWrite.Default); 213 | textureCombined.hideFlags = HideFlags.HideAndDontSave; 214 | } 215 | 216 | if (textureCombinedAlpha == null || (textureCombinedAlpha.width != textureCombined.width || textureCombinedAlpha.height != textureCombined.height)) 217 | { 218 | textureCombinedAlpha = new RenderTexture(textureCombined); 219 | textureCombinedAlpha.hideFlags = HideFlags.HideAndDontSave; 220 | } 221 | } 222 | 223 | void RefreshCombinedTexture(bool preview) 224 | { 225 | UpdateRenderTextures(false); 226 | 227 | if (blitMaterial == null) 228 | { 229 | blitMaterial = new Material(Shader.Find("Hidden/TextureCombiner")); 230 | blitMaterial.name = "Texture Combine"; 231 | blitMaterial.hideFlags = HideFlags.HideAndDontSave; 232 | } 233 | 234 | if ((!preview && removeCompression) || (preview && removeCompressionPreview)) 235 | { 236 | //remove compression of source textures 237 | RemoveCompression(); 238 | } 239 | 240 | blitMaterial.SetTexture("_TexR", textureR); 241 | blitMaterial.SetTexture("_TexG", textureG); 242 | blitMaterial.SetTexture("_TexB", textureB); 243 | blitMaterial.SetTexture("_TexA", textureA); 244 | 245 | blitMaterial.SetFloat("_SrcR", (int)sourceR); 246 | blitMaterial.SetFloat("_SrcG", (int)sourceG); 247 | blitMaterial.SetFloat("_SrcB", (int)sourceB); 248 | blitMaterial.SetFloat("_SrcA", (int)sourceA); 249 | 250 | Graphics.Blit(null, textureCombined, blitMaterial, 0); 251 | Graphics.Blit(textureCombined, textureCombinedAlpha, blitMaterial, 1); 252 | 253 | //restore compression if necessary 254 | RestoreCompression(); 255 | } 256 | 257 | void OnDestroy() 258 | { 259 | if (textureCombined != null) 260 | ClearRenderTexture(textureCombined); 261 | if (textureCombinedAlpha != null) 262 | ClearRenderTexture(textureCombinedAlpha); 263 | if (blitMaterial != null) 264 | DestroyImmediate(blitMaterial); 265 | } 266 | 267 | void ClearRenderTexture(RenderTexture rt) 268 | { 269 | rt.Release(); 270 | DestroyImmediate(rt); 271 | } 272 | 273 | int GUI_SourceChannel(Rect position, Channel channel) 274 | { 275 | var names = System.Enum.GetNames(typeof(Channel)); 276 | var r = position; 277 | r.height /= names.Length; 278 | for (int i = 0; i < names.Length; i++) 279 | { 280 | if (GUI.Toggle(r, (int)channel == i, names[i], EditorStyles.miniButton)) channel = (Channel)i; 281 | r.y += r.height; 282 | } 283 | return (int)channel; 284 | } 285 | 286 | void SaveAs(SaveFormat format) 287 | { 288 | var path = EditorUtility.SaveFilePanelInProject("Save combined texture", "CombinedTexture", (format == SaveFormat.PNG) ? "png" : "exr", "Save combined texture as..."); 289 | if (!string.IsNullOrEmpty(path)) 290 | { 291 | //save to file 292 | var osPath = (Application.dataPath + path.Substring(6)).Replace('/', System.IO.Path.DirectorySeparatorChar); 293 | //blit to render texture 294 | RefreshCombinedTexture(false); 295 | //set active render texture and read pixels 296 | RenderTexture.active = textureCombined; 297 | Texture2D texture2D = new Texture2D(textureCombined.width, textureCombined.height, (format == SaveFormat.PNG) ? TextureFormat.ARGB32 : TextureFormat.RGBAHalf, false); 298 | texture2D.ReadPixels(new Rect(0, 0, textureCombined.width, textureCombined.height), 0, 0); 299 | RenderTexture.active = null; 300 | //save file to disk 301 | byte[] data = (format == SaveFormat.PNG) ? texture2D.EncodeToPNG() : texture2D.EncodeToEXR(Texture2D.EXRFlags.CompressZIP); 302 | System.IO.File.WriteAllBytes(osPath, data); 303 | //import new file in Unity 304 | AssetDatabase.ImportAsset(path); 305 | //set metadata 306 | var importer = AssetImporter.GetAtPath(path); 307 | importer.userData = GetUserData(); 308 | importer.SaveAndReimport(); 309 | //load in UI and select in Project view 310 | textureSaved = AssetDatabase.LoadAssetAtPath(path); 311 | Selection.objects = new Object[] { textureSaved }; 312 | } 313 | } 314 | 315 | Dictionary compressionSettings; 316 | void RemoveCompression() 317 | { 318 | compressionSettings = new Dictionary(); 319 | 320 | CheckTextureCompression(textureR); 321 | CheckTextureCompression(textureG); 322 | CheckTextureCompression(textureB); 323 | CheckTextureCompression(textureA); 324 | } 325 | 326 | void CheckTextureCompression(Texture2D texture) 327 | { 328 | if (texture != null) 329 | { 330 | var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture)) as TextureImporter; 331 | if (importer != null && importer.textureCompression != TextureImporterCompression.Uncompressed) 332 | { 333 | compressionSettings.Add(importer, importer.textureCompression); 334 | importer.textureCompression = TextureImporterCompression.Uncompressed; 335 | importer.SaveAndReimport(); 336 | } 337 | } 338 | } 339 | 340 | void RestoreCompression() 341 | { 342 | if (compressionSettings != null && compressionSettings.Count > 0) 343 | { 344 | foreach (var kvp in compressionSettings) 345 | { 346 | kvp.Key.textureCompression = kvp.Value; 347 | kvp.Key.SaveAndReimport(); 348 | } 349 | } 350 | compressionSettings = null; 351 | } 352 | 353 | bool Load(Texture2D texture) 354 | { 355 | if (texture == null) 356 | return true; 357 | 358 | var importer = AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(texture)); 359 | if (importer != null) 360 | { 361 | if (importer.userData.StartsWith("texture_combiner")) 362 | { 363 | //no error check here! 364 | //may break with different userData 365 | var userDataSplit = importer.userData.Split(' '); 366 | var rGuid = userDataSplit[1].Split(':')[1]; 367 | var gGuid = userDataSplit[2].Split(':')[1]; 368 | var bGuid = userDataSplit[3].Split(':')[1]; 369 | var aGuid = userDataSplit[4].Split(':')[1]; 370 | 371 | textureR = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(rGuid)); 372 | textureG = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(gGuid)); 373 | textureB = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(bGuid)); 374 | textureA = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(aGuid)); 375 | 376 | string errorGUID = ""; 377 | if (!string.IsNullOrEmpty(rGuid) && textureR == null) 378 | { 379 | errorGUID += "Red "; 380 | } 381 | if (!string.IsNullOrEmpty(gGuid) && textureG == null) 382 | { 383 | errorGUID += "Green "; 384 | } 385 | if (!string.IsNullOrEmpty(bGuid) && textureB == null) 386 | { 387 | errorGUID += "Blue "; 388 | } 389 | if (!string.IsNullOrEmpty(aGuid) && textureA == null) 390 | { 391 | errorGUID += "Alpha"; 392 | } 393 | 394 | sourceR = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[5].Split(':')[1]); 395 | sourceG = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[6].Split(':')[1]); 396 | sourceB = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[7].Split(':')[1]); 397 | sourceA = (Channel)System.Enum.Parse(typeof(Channel), userDataSplit[8].Split(':')[1]); 398 | 399 | textureSaved = texture; 400 | if (textureCombined != null) 401 | { 402 | textureCombined.Release(); 403 | DestroyImmediate(textureCombined); 404 | } 405 | if (textureCombinedAlpha != null) 406 | { 407 | textureCombinedAlpha.Release(); 408 | DestroyImmediate(textureCombinedAlpha); 409 | } 410 | 411 | if (!string.IsNullOrEmpty(errorGUID)) 412 | { 413 | EditorUtility.DisplayDialog("Error", "Source texture(s) couldn't be found in the project:\n\n" + errorGUID + "\n\nMaybe they have been deleted, or they GUID has been updated?", "Ok"); 414 | } 415 | 416 | return true; 417 | } 418 | else 419 | { 420 | ShowNotification(new GUIContent("This texture doesn't seem to have been generated with the Texture Combiner")); 421 | } 422 | } 423 | 424 | return false; 425 | } 426 | 427 | string GetUserData() 428 | { 429 | return string.Format("texture_combiner r:{0} g:{1} b:{2} a:{3} rc:{4} gc:{5} bc:{6} ac:{7}", 430 | AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureR)), 431 | AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureG)), 432 | AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureB)), 433 | AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(textureA)), 434 | sourceR, 435 | sourceG, 436 | sourceB, 437 | sourceA 438 | ); 439 | } 440 | } 441 | } -------------------------------------------------------------------------------- /Editor/Edit/NormalMapFixer/NormalFixer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | using UnityEditor; 5 | using System.IO; 6 | using System.Text; 7 | using Unity.EditorCoroutines.Editor; 8 | 9 | // from : https://github.com/Milk-Drinker01/UnityNormalMapInverter 10 | //GUI is based on Diabolickal's HDRP mask map packer, system changed to invert normal maps instead 11 | // updated by gekido to support batch conversion 12 | 13 | namespace PixelWizards.Utilities 14 | { 15 | 16 | public class ConversionModel 17 | { 18 | public string Name; 19 | public string Path; 20 | public Texture NormalMap; 21 | public Texture2D r_NormalMap; 22 | public Texture2D finalTexture; 23 | } 24 | 25 | public class NormalFixer : EditorWindow 26 | { 27 | public List conversionList = new(); 28 | 29 | private Vector2Int texSize; 30 | private static EditorWindow window; 31 | private Vector2 scrollPos; 32 | private int dragDropWidth = 750; 33 | private int dragDropHeight = 150; 34 | 35 | private TextureImporter rawImporter; 36 | private TextureImporterType textureType; 37 | private bool mipmapEnabled; 38 | private bool isReadable; 39 | private FilterMode filterMode; 40 | private TextureImporterNPOTScale npotScale; 41 | private TextureWrapMode wrapMode; 42 | private bool sRGBTexture; 43 | private System.Int32 maxTextureSize; 44 | private TextureImporterCompression textureCompression; 45 | public const string OBJECTBASEPATH = "Assets/Converted/"; 46 | 47 | private string path; 48 | private bool showObjectLog = false; 49 | private StringBuilder logMsg = new StringBuilder(); 50 | private string objectLog = string.Empty; 51 | private GUIStyle log; 52 | private GUIStyle BigBold; 53 | private GUIStyle Wrap; 54 | private GUIStyle subTitle; 55 | private GUIStyle preview; 56 | private bool initialized = false; 57 | 58 | [MenuItem("Edit/Normal Map Correcter")] 59 | public static void ShowWindow() 60 | { 61 | window = GetWindow(typeof(NormalFixer), false); 62 | } 63 | 64 | private void Init() 65 | { 66 | path = OBJECTBASEPATH; 67 | log = new GUIStyle(EditorStyles.label) 68 | { 69 | richText = true 70 | }; 71 | 72 | BigBold = new GUIStyle 73 | { 74 | fontSize = 16, 75 | fontStyle = EditorStyles.boldLabel.fontStyle, 76 | wordWrap = true, 77 | alignment = TextAnchor.MiddleCenter 78 | }; 79 | 80 | Wrap = new GUIStyle 81 | { 82 | wordWrap = true, 83 | alignment = TextAnchor.MiddleCenter 84 | }; 85 | 86 | subTitle = new GUIStyle 87 | { 88 | richText = true, 89 | wordWrap = true, 90 | fontStyle = EditorStyles.boldLabel.fontStyle, 91 | alignment = TextAnchor.MiddleCenter 92 | }; 93 | 94 | preview = new GUIStyle 95 | { 96 | alignment = TextAnchor.UpperCenter 97 | }; 98 | 99 | initialized = true; 100 | } 101 | 102 | private void OnInspectorUpdate() 103 | { 104 | if (!window) 105 | { 106 | window = GetWindow(typeof(NormalFixer), false); 107 | dragDropWidth = (int)window.position.size.x - 15; 108 | } 109 | 110 | if (!initialized) 111 | { 112 | Init(); 113 | } 114 | } 115 | 116 | private void OnGUI() 117 | { 118 | if (!window) 119 | { 120 | window = GetWindow(typeof(NormalFixer), false); 121 | } 122 | 123 | GUILayout.BeginArea(new Rect(0, 0, window.position.size.x, window.position.size.y)); 124 | { 125 | GUILayout.BeginVertical(); 126 | { 127 | GUILayout.Space(10f); 128 | GUILayout.Label("Convert DirectX Normal Map to OpenGL", BigBold); 129 | GUILayout.Space(10f); 130 | GUILayout.Label("Or from OpenGL to DirectX for some reason, you weirdo", subTitle); 131 | GUILayout.Space(10f); 132 | 133 | //Normal Map Input 134 | GUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(window.position.size.x - 15)); 135 | { 136 | GUILayout.Space(10f); 137 | 138 | // NormalMap = (Texture2D)EditorGUILayout.ObjectField("Normal Map", 139 | // NormalMap, 140 | // typeof(Texture2D), 141 | // false); 142 | 143 | // let the users drop objects so they can bulk edit 144 | var textures = DropZone("Drag and Drop Normal Maps Here", (int)window.position.size.x - 25, 145 | dragDropHeight); 146 | if (textures != null) 147 | { 148 | foreach (Object entry in textures) 149 | { 150 | if (entry is not Texture2D texture2D) continue; 151 | 152 | Debug.Log("Entry is texture 2d, adding to list"); 153 | conversionList.Add(new ConversionModel() 154 | { 155 | Name = texture2D.name, 156 | NormalMap = texture2D, 157 | }); 158 | } 159 | } 160 | 161 | GUILayout.Space(10f); 162 | } 163 | GUILayout.EndVertical(); 164 | 165 | if (showObjectLog) 166 | { 167 | GUILayout.Label(objectLog, log); 168 | } 169 | else 170 | { 171 | GUILayout.Label("Convert texture list:", EditorStyles.boldLabel); 172 | scrollPos = GUILayout.BeginScrollView(scrollPos, false, true, GUILayout.ExpandHeight(true)); 173 | { 174 | GUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.ExpandHeight(true)); 175 | { 176 | if (conversionList.Count > 0) 177 | { 178 | for (var index = 0; index < conversionList.Count; index++) 179 | { 180 | var entry = conversionList[index]; 181 | if (entry == null) continue; 182 | GUILayout.BeginHorizontal(); 183 | { 184 | var localPath = path + entry.Name; 185 | GUILayout.Label( 186 | "\t" + entry.Name + "\tPath: " + 187 | localPath + 188 | "", log); 189 | if (GUILayout.Button("X", GUILayout.Width(20f), GUILayout.Height(20f))) 190 | { 191 | RemoveEntry(entry); 192 | } 193 | 194 | GUILayout.Space(5f); 195 | } 196 | GUILayout.EndHorizontal(); 197 | } 198 | } 199 | } 200 | GUILayout.EndVertical(); 201 | } 202 | GUILayout.EndScrollView(); 203 | GUILayout.Space(15f); 204 | } 205 | 206 | GUILayout.Space(15); 207 | GUILayout.Label("Current Save Path: " + path, EditorStyles.boldLabel); 208 | GUILayout.Space(15); 209 | GUILayout.BeginHorizontal(); 210 | { 211 | if (conversionList.Count > 0) 212 | { 213 | if (GUILayout.Button("Invert Normal Maps", GUILayout.Width(250f), GUILayout.Height(35f))) 214 | { 215 | EditorCoroutineUtility.StartCoroutine(PackTextures(), this); 216 | } 217 | } 218 | 219 | if (GUILayout.Button("Change Path", GUILayout.Width(250f), GUILayout.Height(35f))) 220 | { 221 | path = EditorUtility.SaveFolderPanel("Save converted textures to folder:", path, 222 | OBJECTBASEPATH); 223 | path += "/"; 224 | 225 | if (!Directory.Exists(path)) 226 | { 227 | GUILayout.Label(objectLog, log); 228 | } 229 | } 230 | 231 | if (GUILayout.Button("Clear List)", GUILayout.Width(250f), GUILayout.Height(35f))) 232 | { 233 | Log("Clearing conversion list..."); 234 | conversionList.Clear(); 235 | logMsg.Clear(); 236 | } 237 | } 238 | GUILayout.EndHorizontal(); 239 | GUILayout.Space(25); 240 | 241 | } 242 | GUILayout.EndVertical(); 243 | } 244 | GUILayout.EndArea(); 245 | } 246 | 247 | private void RemoveEntry(ConversionModel entry) 248 | { 249 | conversionList.Remove(entry); 250 | } 251 | 252 | /// 253 | /// Returns a list of objects that were dropped on us 254 | /// 255 | /// 256 | /// 257 | /// 258 | /// 259 | public static object[] DropZone(string title, int w, int h) 260 | { 261 | GUILayout.Box(title, GUILayout.Width(w), GUILayout.Height(h)); 262 | 263 | EventType eventType = Event.current.type; 264 | bool isAccepted = false; 265 | 266 | if (eventType != EventType.DragUpdated && eventType != EventType.DragPerform) 267 | return isAccepted ? DragAndDrop.objectReferences : null; 268 | 269 | DragAndDrop.visualMode = DragAndDropVisualMode.Copy; 270 | 271 | if (eventType == EventType.DragPerform) 272 | { 273 | DragAndDrop.AcceptDrag(); 274 | isAccepted = true; 275 | } 276 | 277 | Event.current.Use(); 278 | 279 | if (isAccepted) 280 | { 281 | Debug.Log("Drag and Drop complete: " + DragAndDrop.objectReferences.Length + " objects dropped!"); 282 | } 283 | 284 | return isAccepted ? DragAndDrop.objectReferences : null; 285 | } 286 | 287 | private IEnumerator PackTextures() 288 | { 289 | if (conversionList.Count <= 0) yield break; 290 | 291 | var interval = 1 / conversionList.Count; 292 | var progress = 0; 293 | var msg = "Packing " + conversionList.Count + " Textures, please wait..."; 294 | EditorUtility.DisplayProgressBar(msg, "", 0f); 295 | yield return new WaitForSeconds(1f); 296 | Log(msg); 297 | foreach (var entry in conversionList) 298 | { 299 | progress += interval; 300 | var filePath = path + entry.Name + ".png"; 301 | msg = "Packing " + entry.Name + " to path: + " + filePath + "please wait..."; 302 | EditorUtility.DisplayProgressBar(msg, "", progress); 303 | yield return new WaitForSeconds(0.1f); 304 | Log(msg); 305 | // copy and convert the texture into entry.finalTexture 306 | UpdateTexture(entry, false); 307 | yield return new WaitForSeconds(0.1f); 308 | 309 | Log("Encoding PNG..."); 310 | var pngData = entry.finalTexture.EncodeToPNG(); 311 | yield return new WaitForSeconds(0.1f); 312 | if (filePath.Length != 0) 313 | { 314 | if (pngData != null) 315 | { 316 | Log("Writing file to path: " + filePath); 317 | File.WriteAllBytes(filePath, pngData); 318 | } 319 | } 320 | yield return new WaitForSeconds(0.1f); 321 | Log("Refresh Asset database..."); 322 | AssetDatabase.Refresh(); 323 | yield return new WaitForSeconds(0.1f); 324 | 325 | Log("revert original texture settings"); 326 | //restore original texture settings 327 | rawImporter.textureType = textureType; 328 | rawImporter.mipmapEnabled = mipmapEnabled; 329 | rawImporter.isReadable = isReadable; 330 | rawImporter.filterMode = filterMode; 331 | rawImporter.npotScale = npotScale; 332 | rawImporter.wrapMode = wrapMode; 333 | rawImporter.sRGBTexture = sRGBTexture; 334 | rawImporter.maxTextureSize = maxTextureSize; 335 | rawImporter.textureCompression = textureCompression; 336 | rawImporter.SaveAndReimport(); 337 | 338 | Log("Save and Reimport..."); 339 | // reset new normal settings 340 | TextureImporter NormalImporter = (TextureImporter)AssetImporter.GetAtPath(filePath); 341 | NormalImporter.textureType = TextureImporterType.NormalMap; 342 | NormalImporter.maxTextureSize = maxTextureSize; 343 | NormalImporter.SaveAndReimport(); 344 | yield return new WaitForSeconds(0.1f); 345 | Log("Texture Saved to: " + filePath); 346 | } 347 | 348 | msg = "Completed packing textures!"; 349 | EditorUtility.DisplayProgressBar(msg, "", 1f); 350 | yield return new WaitForSeconds(1f); 351 | Log(msg); 352 | EditorUtility.ClearProgressBar(); 353 | yield return new WaitForSeconds(1f); 354 | } 355 | 356 | private void UpdateTexture(ConversionModel entry, bool asPreview) 357 | { 358 | Log("Update Texture(): " + entry.Name); 359 | Log("Get Raw Normal Map"); 360 | entry.r_NormalMap = (Texture2D)GetRawTexture(entry.NormalMap); 361 | Log("Create Final Texture"); 362 | entry.finalTexture = new Texture2D(texSize.x, texSize.y, TextureFormat.RGBAFloat, true); 363 | Log("Converting..."); 364 | for (int x = 0; x < texSize.x; x++) 365 | { 366 | for (int y = 0; y < texSize.y; y++) 367 | { 368 | float R, G, B; 369 | R = entry.r_NormalMap.GetPixel(x, y).r; 370 | //R = r_NormalMap.getr(x, y).r; 371 | G = 1 - entry.r_NormalMap.GetPixel(x, y).g; 372 | B = entry.r_NormalMap.GetPixel(x, y).b; 373 | 374 | entry.finalTexture.SetPixel(x, y, new Color(R, G, B)); 375 | } 376 | } 377 | 378 | Log("Conversion completed, applying to final texture..."); 379 | entry.finalTexture.Apply(); 380 | } 381 | 382 | private Texture GetRawTexture(Texture original, bool sRGBFallback = false) 383 | { 384 | string originalPath = AssetDatabase.GetAssetPath(original); 385 | rawImporter = (TextureImporter)AssetImporter.GetAtPath(originalPath); 386 | 387 | //get current settings 388 | textureType = rawImporter.textureType; 389 | mipmapEnabled = rawImporter.mipmapEnabled; 390 | isReadable = rawImporter.isReadable; 391 | filterMode = rawImporter.filterMode; 392 | npotScale = rawImporter.npotScale; 393 | wrapMode = rawImporter.wrapMode; 394 | sRGBTexture = rawImporter.sRGBTexture; 395 | maxTextureSize = rawImporter.maxTextureSize; 396 | textureCompression = rawImporter.textureCompression; 397 | 398 | //set the required setings for the conversion 399 | rawImporter.textureType = TextureImporterType.Default; 400 | rawImporter.mipmapEnabled = false; 401 | rawImporter.isReadable = true; 402 | //rawImporter.filterMode = m_bilinearFilter ? FilterMode.Bilinear : FilterMode.Point; 403 | rawImporter.filterMode = true ? FilterMode.Bilinear : FilterMode.Point; 404 | rawImporter.npotScale = TextureImporterNPOTScale.None; 405 | rawImporter.wrapMode = TextureWrapMode.Clamp; 406 | 407 | int w, h; 408 | rawImporter.GetSourceTextureWidthAndHeight(out w, out h); 409 | texSize = new Vector2Int(w, h); 410 | 411 | Texture2D originalTex2D = original as Texture2D; 412 | rawImporter.sRGBTexture = (originalTex2D == null) 413 | ? sRGBFallback 414 | : (AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(original)) as TextureImporter).sRGBTexture; 415 | 416 | rawImporter.maxTextureSize = 8192; 417 | 418 | rawImporter.textureCompression = TextureImporterCompression.Uncompressed; 419 | 420 | rawImporter.SaveAndReimport(); 421 | 422 | return AssetDatabase.LoadAssetAtPath(originalPath); 423 | } 424 | 425 | private void Log(string msg) 426 | { 427 | logMsg.AppendLine(msg); 428 | Debug.Log(msg); 429 | } 430 | } 431 | } -------------------------------------------------------------------------------- /Editor/Assets/BatchExtractMaterials.cs: -------------------------------------------------------------------------------- 1 | using UnityEngine; 2 | using UnityEditor; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | 6 | namespace PixelWizards.Utilities 7 | { 8 | public class BatchExtractMaterials : EditorWindow 9 | { 10 | private enum ExtractMode { Extract = 0, Remap = 1, Ignore = 2 }; 11 | 12 | [System.Serializable] 13 | private class ExtractData 14 | { 15 | public GameObject model; 16 | 17 | public List materialNames = new List(); 18 | public List originalMaterials = new List(); 19 | public List remappedMaterials = new List(); 20 | public List materialExtractModes = new List(); 21 | 22 | public ExtractData() { } 23 | public ExtractData( GameObject model ) { this.model = model; } 24 | } 25 | 26 | private class RemapAllPopup : EditorWindow 27 | { 28 | private List remapFrom = new List( 2 ); 29 | private Material remapTo; 30 | private bool skipIgnoredMaterials; 31 | 32 | private Vector2 scrollPos; 33 | 34 | private System.Action, Material, bool> onRemapConfirmed; 35 | 36 | public static void ShowAt( Rect buttonRect, Vector2 size, System.Action, Material, bool> onRemapConfirmed ) 37 | { 38 | buttonRect.position = GUIUtility.GUIToScreenPoint( buttonRect.position ); 39 | 40 | remapAllPopup = GetWindow( true ); 41 | remapAllPopup.position = new Rect( buttonRect.position + new Vector2( ( buttonRect.width - size.x ) * 0.5f, buttonRect.height ), size ); 42 | remapAllPopup.minSize = size; 43 | remapAllPopup.titleContent = new GUIContent( "Remap All..." ); 44 | remapAllPopup.skipIgnoredMaterials = EditorPrefs.GetBool( "BEM_SkipIgnoredMats", true ); 45 | remapAllPopup.onRemapConfirmed = onRemapConfirmed; 46 | remapAllPopup.scrollPos = Vector2.zero; 47 | remapAllPopup.Show(); 48 | } 49 | 50 | public static void Hide() 51 | { 52 | if( remapAllPopup ) 53 | { 54 | remapAllPopup.Close(); 55 | remapAllPopup = null; 56 | } 57 | } 58 | 59 | private void OnDestroy() 60 | { 61 | remapAllPopup = null; 62 | } 63 | 64 | private void OnGUI() 65 | { 66 | if( !remapAllPopup ) 67 | { 68 | Close(); 69 | GUIUtility.ExitGUI(); 70 | } 71 | 72 | Event ev = Event.current; 73 | 74 | EditorGUILayout.LabelField( "This will find all materials that point to 'Remap From' and remap them to 'Remap To'. If 'Remap From' is empty, all materials will be remapped to 'Remap To'.", EditorStyles.wordWrappedLabel ); 75 | 76 | scrollPos = EditorGUILayout.BeginScrollView( scrollPos ); 77 | 78 | GUILayout.BeginHorizontal(); 79 | GUILayout.Label( "Remap From (drag & drop here)" ); 80 | 81 | if( remapFrom.Count == 0 ) 82 | remapFrom.Add( null ); 83 | 84 | // Allow drag & dropping materials to array 85 | // Credit: https://answers.unity.com/answers/657877/view.html 86 | if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 87 | { 88 | DragAndDrop.visualMode = DragAndDropVisualMode.Copy; 89 | if( ev.type == EventType.DragPerform ) 90 | { 91 | DragAndDrop.AcceptDrag(); 92 | 93 | Object[] draggedObjects = DragAndDrop.objectReferences; 94 | for( int i = 0; i < draggedObjects.Length; i++ ) 95 | { 96 | Material material = draggedObjects[i] as Material; 97 | if( !material ) 98 | continue; 99 | 100 | if( !remapFrom.Contains( material ) ) 101 | { 102 | bool replacedNullElement = false; 103 | for( int j = 0; j < remapFrom.Count; j++ ) 104 | { 105 | if( !remapFrom[j] ) 106 | { 107 | remapFrom[j] = material; 108 | replacedNullElement = true; 109 | break; 110 | } 111 | } 112 | 113 | if( !replacedNullElement ) 114 | remapFrom.Add( material ); 115 | } 116 | } 117 | } 118 | 119 | ev.Use(); 120 | } 121 | 122 | if( GUILayout.Button( "+", GL_WIDTH_25 ) ) 123 | remapFrom.Insert( 0, null ); 124 | 125 | GUILayout.EndHorizontal(); 126 | 127 | for( int i = 0; i < remapFrom.Count; i++ ) 128 | { 129 | GUILayout.BeginHorizontal(); 130 | 131 | remapFrom[i] = EditorGUILayout.ObjectField( GUIContent.none, remapFrom[i], typeof( Material ), false ) as Material; 132 | 133 | if( GUILayout.Button( "+", GL_WIDTH_25 ) ) 134 | remapFrom.Insert( i + 1, null ); 135 | 136 | if( GUILayout.Button( "-", GL_WIDTH_25 ) ) 137 | { 138 | // Lists with no elements look ugly, always keep a dummy null variable 139 | if( remapFrom.Count > 1 ) 140 | remapFrom.RemoveAt( i-- ); 141 | else 142 | remapFrom[0] = null; 143 | } 144 | 145 | GUILayout.EndHorizontal(); 146 | } 147 | 148 | EditorGUILayout.EndScrollView(); 149 | 150 | remapTo = EditorGUILayout.ObjectField( "Remap To", remapTo, typeof( Material ), false ) as Material; 151 | 152 | EditorGUI.BeginChangeCheck(); 153 | skipIgnoredMaterials = EditorGUILayout.Toggle( "Skip Ignored Materials", skipIgnoredMaterials ); 154 | if( EditorGUI.EndChangeCheck() ) 155 | EditorPrefs.SetBool( "BEM_SkipIgnoredMats", skipIgnoredMaterials ); 156 | 157 | EditorGUILayout.Space(); 158 | 159 | GUILayout.BeginHorizontal(); 160 | if( GUILayout.Button( "Cancel" ) ) 161 | Close(); 162 | if( GUILayout.Button( "Apply" ) ) 163 | { 164 | if( remapTo && onRemapConfirmed != null ) 165 | { 166 | bool remapFromIsFilled = false; 167 | for( int i = 0; i < remapFrom.Count; i++ ) 168 | { 169 | if( remapFrom[i] ) 170 | { 171 | remapFromIsFilled = true; 172 | break; 173 | } 174 | } 175 | 176 | onRemapConfirmed( remapFromIsFilled ? remapFrom : null, remapTo, skipIgnoredMaterials ); 177 | } 178 | 179 | Close(); 180 | } 181 | GUILayout.EndHorizontal(); 182 | 183 | GUILayout.Space( 5f ); 184 | } 185 | } 186 | 187 | private const string HELP_TEXT = 188 | "- Extract: material will be extracted to the destination folder\n" + 189 | "- Remap: material will be remapped to an existing material asset" + 190 | #if UNITY_2019_1_OR_NEWER 191 | " (when Remap is the default value, then it means that a material that satisfies 'Default Material Remap Conditions' was found)" + 192 | #endif 193 | ". If Remap points to an embedded material, then that embedded material will first be extracted\n" + 194 | "- Ignore: material's current value will stay intact (when Ignore is the default value, either the material couldn't be found " + 195 | "or it was already extracted)"; 196 | 197 | private static readonly GUILayoutOption GL_WIDTH_25 = GUILayout.Width( 25f ); 198 | private readonly GUILayoutOption GL_WIDTH_75 = GUILayout.Width( 75f ); 199 | private readonly GUILayoutOption GL_MIN_WIDTH_50 = GUILayout.MinWidth( 50f ); 200 | 201 | private string materialsFolder = "Assets/Materials"; 202 | private List modelData = new List( 16 ); 203 | 204 | #if UNITY_2019_1_OR_NEWER 205 | private bool remappedMaterialNamesMustMatch = false; 206 | private bool remappedMaterialPropertiesMustMatch = true; 207 | private bool dontRemapExtractedMaterials = true; 208 | private bool dontRemapMaterialsAcrossDifferentModels = false; 209 | #endif 210 | 211 | private bool inModelSelectionPhase = true; 212 | 213 | private Rect remapAllButtonRect; 214 | private static RemapAllPopup remapAllPopup; 215 | 216 | private Vector2 scrollPos; 217 | 218 | [MenuItem( "Assets/Batch Extract Materials" )] 219 | private static void Init() 220 | { 221 | BatchExtractMaterials window = GetWindow(); 222 | window.titleContent = new GUIContent( "Extract Materials" ); 223 | window.minSize = new Vector2( 300f, 120f ); 224 | window.Show(); 225 | } 226 | 227 | private void OnDestroy() 228 | { 229 | // Close RemapAllPopup with this window 230 | RemapAllPopup.Hide(); 231 | } 232 | 233 | private void OnFocus() 234 | { 235 | // Don't let RemapAllPopup be obstructed by this window 236 | // We are using delayCall because otherwise clicking an ObjectField in this window doesn't highlight that material in the Project window 237 | EditorApplication.delayCall += () => 238 | { 239 | if( remapAllPopup ) 240 | remapAllPopup.Focus(); 241 | }; 242 | } 243 | 244 | private void OnGUI() 245 | { 246 | scrollPos = EditorGUILayout.BeginScrollView( scrollPos ); 247 | 248 | GUI.enabled = inModelSelectionPhase; 249 | DrawDestinationPathField(); 250 | DrawModelsToProcessList(); 251 | DrawMaterialRemapConditionsField(); 252 | GUI.enabled = true; 253 | 254 | bool modelsToProcessListIsFilled = modelData.Find( ( data ) => data.model ) != null; 255 | 256 | if( inModelSelectionPhase ) 257 | { 258 | GUI.enabled = modelsToProcessListIsFilled && !string.IsNullOrEmpty( materialsFolder ) && materialsFolder.StartsWith( "Assets" ); 259 | if( GUILayout.Button( "Next" ) ) 260 | { 261 | inModelSelectionPhase = false; 262 | CalculateRemappedMaterials(); 263 | 264 | GUIUtility.ExitGUI(); 265 | } 266 | } 267 | else 268 | { 269 | DrawMaterialRemapList(); 270 | 271 | GUILayout.BeginHorizontal(); 272 | 273 | if( GUILayout.Button( "Back" ) ) 274 | { 275 | inModelSelectionPhase = true; 276 | RemapAllPopup.Hide(); 277 | 278 | GUIUtility.ExitGUI(); 279 | } 280 | 281 | Color c = GUI.backgroundColor; 282 | GUI.backgroundColor = Color.green; 283 | 284 | GUI.enabled = modelsToProcessListIsFilled; 285 | if( GUILayout.Button( "Extract!" ) ) 286 | { 287 | inModelSelectionPhase = true; 288 | RemapAllPopup.Hide(); 289 | 290 | ExtractMaterials(); 291 | GUIUtility.ExitGUI(); 292 | } 293 | 294 | GUI.backgroundColor = c; 295 | GUILayout.EndHorizontal(); 296 | } 297 | 298 | GUI.enabled = true; 299 | 300 | EditorGUILayout.Space(); 301 | EditorGUILayout.EndScrollView(); 302 | } 303 | 304 | private void DrawDestinationPathField() 305 | { 306 | Event ev = Event.current; 307 | 308 | GUILayout.BeginHorizontal(); 309 | 310 | materialsFolder = EditorGUILayout.TextField( "Extract Materials To", materialsFolder ); 311 | 312 | // Allow drag & dropping a folder to the text field 313 | // Credit: https://answers.unity.com/answers/657877/view.html 314 | if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 315 | { 316 | DragAndDrop.visualMode = DragAndDropVisualMode.Copy; 317 | if( ev.type == EventType.DragPerform ) 318 | { 319 | DragAndDrop.AcceptDrag(); 320 | 321 | string[] draggedFiles = DragAndDrop.paths; 322 | for( int i = 0; i < draggedFiles.Length; i++ ) 323 | { 324 | if( !string.IsNullOrEmpty( draggedFiles[i] ) && AssetDatabase.IsValidFolder( draggedFiles[i] ) ) 325 | { 326 | materialsFolder = draggedFiles[i]; 327 | break; 328 | } 329 | } 330 | } 331 | 332 | ev.Use(); 333 | } 334 | 335 | if( GUILayout.Button( "o", GL_WIDTH_25 ) ) 336 | { 337 | string selectedPath = EditorUtility.OpenFolderPanel( "Choose output directory", "Assets", "" ); 338 | if( !string.IsNullOrEmpty( selectedPath ) ) 339 | { 340 | selectedPath = selectedPath.Replace( '\\', '/' ) + "/"; 341 | 342 | int relativePathIndex = selectedPath.IndexOf( "/Assets/" ) + 1; 343 | if( relativePathIndex > 0 ) 344 | materialsFolder = selectedPath.Substring( relativePathIndex, selectedPath.Length - relativePathIndex - 1 ); 345 | } 346 | 347 | GUIUtility.keyboardControl = 0; // Remove focus from active text field 348 | } 349 | 350 | GUILayout.EndHorizontal(); 351 | EditorGUILayout.Space(); 352 | } 353 | 354 | private void DrawModelsToProcessList() 355 | { 356 | Event ev = Event.current; 357 | 358 | GUILayout.BeginHorizontal(); 359 | GUILayout.Label( "Models To Process (drag & drop here)" ); 360 | 361 | if( modelData.Count == 0 ) 362 | modelData.Add( new ExtractData() ); 363 | 364 | // Allow drag & dropping models to array 365 | // Credit: https://answers.unity.com/answers/657877/view.html 366 | if( ( ev.type == EventType.DragPerform || ev.type == EventType.DragUpdated ) && GUILayoutUtility.GetLastRect().Contains( ev.mousePosition ) ) 367 | { 368 | DragAndDrop.visualMode = DragAndDropVisualMode.Copy; 369 | if( ev.type == EventType.DragPerform ) 370 | { 371 | DragAndDrop.AcceptDrag(); 372 | 373 | Object[] draggedObjects = DragAndDrop.objectReferences; 374 | for( int i = 0; i < draggedObjects.Length; i++ ) 375 | { 376 | if( !( draggedObjects[i] as GameObject ) || PrefabUtility.GetPrefabAssetType( draggedObjects[i] ) != PrefabAssetType.Model ) 377 | continue; 378 | 379 | bool modelAlreadyExists = false; 380 | for( int j = 0; j < modelData.Count; j++ ) 381 | { 382 | if( modelData[j].model == draggedObjects[i] ) 383 | { 384 | modelAlreadyExists = true; 385 | break; 386 | } 387 | } 388 | 389 | if( !modelAlreadyExists ) 390 | { 391 | bool replacedNullElement = false; 392 | for( int j = 0; j < modelData.Count; j++ ) 393 | { 394 | if( !modelData[j].model ) 395 | { 396 | modelData[j] = new ExtractData( draggedObjects[i] as GameObject ); 397 | replacedNullElement = true; 398 | break; 399 | } 400 | } 401 | 402 | if( !replacedNullElement ) 403 | modelData.Add( new ExtractData( draggedObjects[i] as GameObject ) ); 404 | } 405 | } 406 | } 407 | 408 | ev.Use(); 409 | } 410 | 411 | if( GUILayout.Button( "+", GL_WIDTH_25 ) ) 412 | modelData.Insert( 0, new ExtractData() ); 413 | 414 | GUILayout.EndHorizontal(); 415 | 416 | for( int i = 0; i < modelData.Count; i++ ) 417 | { 418 | ExtractData element = modelData[i]; 419 | 420 | GUI.changed = false; 421 | GUILayout.BeginHorizontal(); 422 | 423 | GameObject prevObject = element.model; 424 | GameObject newObject = EditorGUILayout.ObjectField( GUIContent.none, prevObject, typeof( GameObject ), false ) as GameObject; 425 | if( newObject && PrefabUtility.GetPrefabAssetType( newObject ) != PrefabAssetType.Model ) 426 | newObject = prevObject; 427 | 428 | modelData[i].model = newObject; 429 | 430 | if( GUILayout.Button( "+", GL_WIDTH_25 ) ) 431 | modelData.Insert( i + 1, new ExtractData() ); 432 | 433 | if( GUILayout.Button( "-", GL_WIDTH_25 ) ) 434 | { 435 | // Lists with no elements look ugly, always keep a dummy null variable 436 | if( modelData.Count > 1 ) 437 | modelData.RemoveAt( i-- ); 438 | else 439 | modelData[0] = new ExtractData(); 440 | } 441 | 442 | GUILayout.EndHorizontal(); 443 | } 444 | 445 | EditorGUILayout.Space(); 446 | } 447 | 448 | private void DrawMaterialRemapConditionsField() 449 | { 450 | #if UNITY_2019_1_OR_NEWER 451 | EditorGUILayout.LabelField( "Default Material Remap Conditions" ); 452 | EditorGUI.indentLevel++; 453 | 454 | remappedMaterialNamesMustMatch = EditorGUILayout.ToggleLeft( "Material names must match", remappedMaterialNamesMustMatch ); 455 | remappedMaterialPropertiesMustMatch = EditorGUILayout.ToggleLeft( "Material properties must match", remappedMaterialPropertiesMustMatch ); 456 | dontRemapExtractedMaterials = EditorGUILayout.ToggleLeft( "Don't remap already extracted materials", dontRemapExtractedMaterials ); 457 | dontRemapMaterialsAcrossDifferentModels = EditorGUILayout.ToggleLeft( "Don't remap Model A's materials to Model B (i.e. different models won't share the same materials)", dontRemapMaterialsAcrossDifferentModels ); 458 | 459 | EditorGUI.indentLevel--; 460 | EditorGUILayout.Space(); 461 | #endif 462 | } 463 | 464 | private void DrawMaterialRemapList() 465 | { 466 | EditorGUILayout.HelpBox( HELP_TEXT, MessageType.Info ); 467 | 468 | GUILayout.BeginHorizontal(); 469 | 470 | if( GUILayout.Button( "Extract All" ) ) 471 | { 472 | for( int i = 0; i < modelData.Count; i++ ) 473 | { 474 | for( int j = 0; j < modelData[i].materialExtractModes.Count; j++ ) 475 | modelData[i].materialExtractModes[j] = ExtractMode.Extract; 476 | } 477 | } 478 | 479 | if( GUILayout.Button( "Remap All..." ) ) 480 | { 481 | RemapAllPopup.ShowAt( remapAllButtonRect, new Vector2( 325f, 250f ), ( List remapFrom, Material remapTo, bool skipIgnoredMaterials ) => 482 | { 483 | for( int i = 0; i < modelData.Count; i++ ) 484 | { 485 | ExtractData data = modelData[i]; 486 | for( int j = 0; j < data.remappedMaterials.Count; j++ ) 487 | { 488 | switch( data.materialExtractModes[j] ) 489 | { 490 | case ExtractMode.Extract: 491 | { 492 | if( remapFrom == null || ( data.originalMaterials[j] && remapFrom.Contains( data.originalMaterials[j] ) ) ) 493 | { 494 | data.materialExtractModes[j] = ExtractMode.Remap; 495 | data.remappedMaterials[j] = remapTo; 496 | } 497 | 498 | break; 499 | } 500 | case ExtractMode.Remap: 501 | { 502 | if( remapFrom == null || ( data.remappedMaterials[j] && remapFrom.Contains( data.remappedMaterials[j] ) ) ) 503 | data.remappedMaterials[j] = remapTo; 504 | 505 | break; 506 | } 507 | case ExtractMode.Ignore: 508 | { 509 | if( !skipIgnoredMaterials && ( remapFrom == null || ( data.originalMaterials[j] && remapFrom.Contains( data.originalMaterials[j] ) ) ) ) 510 | { 511 | data.materialExtractModes[j] = ExtractMode.Remap; 512 | data.remappedMaterials[j] = remapTo; 513 | } 514 | 515 | break; 516 | } 517 | } 518 | } 519 | } 520 | 521 | Repaint(); 522 | } ); 523 | } 524 | 525 | if( Event.current.type == EventType.Repaint ) 526 | remapAllButtonRect = GUILayoutUtility.GetLastRect(); 527 | 528 | if( GUILayout.Button( "Ignore All" ) ) 529 | { 530 | for( int i = 0; i < modelData.Count; i++ ) 531 | { 532 | for( int j = 0; j < modelData[i].materialExtractModes.Count; j++ ) 533 | modelData[i].materialExtractModes[j] = ExtractMode.Ignore; 534 | } 535 | } 536 | 537 | GUILayout.EndHorizontal(); 538 | 539 | EditorGUILayout.Space(); 540 | 541 | for( int i = 0; i < modelData.Count; i++ ) 542 | { 543 | ExtractData data = modelData[i]; 544 | if( !data.model ) 545 | continue; 546 | 547 | GUI.enabled = false; 548 | EditorGUILayout.ObjectField( GUIContent.none, data.model, typeof( GameObject ), false ); 549 | GUI.enabled = true; 550 | 551 | if( data.originalMaterials.Count == 0 ) 552 | EditorGUILayout.LabelField( "This model has no materials..." ); 553 | 554 | for( int j = 0; j < data.originalMaterials.Count; j++ ) 555 | { 556 | GUILayout.BeginHorizontal(); 557 | 558 | EditorGUILayout.PrefixLabel( data.materialNames[j] ); 559 | 560 | data.materialExtractModes[j] = (ExtractMode) EditorGUILayout.EnumPopup( GUIContent.none, data.materialExtractModes[j], GL_WIDTH_75 ); 561 | if( data.materialExtractModes[j] == ExtractMode.Remap ) 562 | { 563 | EditorGUI.BeginChangeCheck(); 564 | data.remappedMaterials[j] = EditorGUILayout.ObjectField( GUIContent.none, data.remappedMaterials[j], typeof( Material ), false, GL_MIN_WIDTH_50 ) as Material; 565 | if( EditorGUI.EndChangeCheck() && ( !data.remappedMaterials[j] || data.remappedMaterials[j] == data.originalMaterials[j] ) ) 566 | data.materialExtractModes[j] = ExtractMode.Ignore; 567 | } 568 | else 569 | { 570 | GUI.enabled = false; 571 | EditorGUILayout.ObjectField( GUIContent.none, data.originalMaterials[j], typeof( Material ), false, GL_MIN_WIDTH_50 ); 572 | GUI.enabled = true; 573 | } 574 | 575 | GUILayout.EndHorizontal(); 576 | } 577 | 578 | EditorGUILayout.Space(); 579 | } 580 | } 581 | 582 | private void CalculateRemappedMaterials() 583 | { 584 | #if UNITY_2019_1_OR_NEWER 585 | // Key: Material CRC (material.ComputeCRC) 586 | // Value: All materials sharing that CRC 587 | Dictionary> duplicateMaterialsLookup = new Dictionary>( modelData.Count * 8 ); 588 | 589 | // Add all existing materials at materialsFolder to the lookup table 590 | if( !dontRemapMaterialsAcrossDifferentModels && Directory.Exists( materialsFolder ) ) 591 | { 592 | string[] existingMaterialPaths = Directory.GetFiles( materialsFolder, "*.mat", SearchOption.TopDirectoryOnly ); 593 | for( int i = 0; i < existingMaterialPaths.Length; i++ ) 594 | { 595 | Material material = AssetDatabase.LoadMainAssetAtPath( existingMaterialPaths[i] ) as Material; 596 | if( material ) 597 | GetMaterialsWithCRC( duplicateMaterialsLookup, material ).Add( material ); 598 | } 599 | } 600 | #endif 601 | 602 | for( int i = 0; i < modelData.Count; i++ ) 603 | { 604 | ExtractData data = modelData[i]; 605 | if( !data.model ) 606 | { 607 | modelData.RemoveAt( i-- ); 608 | continue; 609 | } 610 | 611 | string modelPath = AssetDatabase.GetAssetPath( data.model ); 612 | ModelImporter modelImporter = AssetImporter.GetAtPath( modelPath ) as ModelImporter; 613 | if( !modelImporter ) 614 | { 615 | Debug.LogWarning( "Couldn't get ModelImporter from asset: " + AssetDatabase.GetAssetPath( data.model ), data.model ); 616 | modelData.RemoveAt( i-- ); 617 | continue; 618 | } 619 | 620 | // Reset previously assigned values to this entry (if any) 621 | data = modelData[i] = new ExtractData( data.model ); 622 | 623 | Object[] embeddedAssets = AssetDatabase.LoadAllAssetRepresentationsAtPath( modelPath ); 624 | List embeddedMaterials = new List( embeddedAssets.Length ); 625 | for( int j = 0; j < embeddedAssets.Length; j++ ) 626 | { 627 | Material embeddedMaterial = embeddedAssets[j] as Material; 628 | if( embeddedMaterial ) 629 | embeddedMaterials.Add( embeddedMaterial ); 630 | } 631 | 632 | // Get the model's current material remapping 633 | // Credit: https://forum.unity.com/threads/batch-change-all-fbx-default-materials-help.626341/#post-6530939 634 | using( SerializedObject so = new SerializedObject( modelImporter ) ) 635 | { 636 | SerializedProperty materials = so.FindProperty( "m_Materials" ); 637 | SerializedProperty externalObjects = so.FindProperty( "m_ExternalObjects" ); 638 | 639 | for( int materialIndex = 0; materialIndex < materials.arraySize; materialIndex++ ) 640 | { 641 | SerializedProperty id = materials.GetArrayElementAtIndex( materialIndex ); 642 | string name = id.FindPropertyRelative( "name" ).stringValue; 643 | string type = id.FindPropertyRelative( "type" ).stringValue; 644 | 645 | Material material = null; 646 | for( int externalObjectIndex = 0; externalObjectIndex < externalObjects.arraySize; externalObjectIndex++ ) 647 | { 648 | SerializedProperty pair = externalObjects.GetArrayElementAtIndex( externalObjectIndex ); 649 | string externalName = pair.FindPropertyRelative( "first.name" ).stringValue; 650 | string externalType = pair.FindPropertyRelative( "first.type" ).stringValue; 651 | 652 | if( externalType == type && externalName == name && ( pair = pair.FindPropertyRelative( "second" ) ) != null ) 653 | { 654 | material = pair.objectReferenceValue as Material; 655 | break; 656 | } 657 | } 658 | 659 | if( !material ) 660 | material = embeddedMaterials.Find( ( m ) => m.name == name ); 661 | 662 | data.materialNames.Add( name ); 663 | data.originalMaterials.Add( material ); 664 | 665 | if( !material ) 666 | { 667 | data.materialExtractModes.Add( ExtractMode.Ignore ); 668 | data.remappedMaterials.Add( null ); 669 | } 670 | else 671 | { 672 | bool materialAlreadyExtracted = AssetDatabase.IsMainAsset( material ); 673 | #if UNITY_2019_1_OR_NEWER 674 | HashSet duplicateMaterials = GetMaterialsWithCRC( duplicateMaterialsLookup, material ); 675 | Material remappedMaterial = null; 676 | 677 | // - Material was already extracted: remap the material only if 'dontRemapExtractedMaterials' is false 678 | // - 'dontRemapMaterialsAcrossDifferentModels' is true: only remap with a material from the same model 679 | // - 'remappedMaterialPropertiesMustMatch' is true: only remap with a material whose properties match 680 | // the current material's properties 681 | // - Only 'remappedMaterialNamesMustMatch' is true: remap with a material with the same name; properties 682 | // of the two materials may not match 683 | if( !materialAlreadyExtracted || !dontRemapExtractedMaterials ) 684 | { 685 | if( remappedMaterialPropertiesMustMatch ) 686 | { 687 | foreach( Material _material in duplicateMaterials ) 688 | { 689 | if( _material.name == name || ( !remappedMaterial && !remappedMaterialNamesMustMatch ) ) 690 | remappedMaterial = _material; 691 | } 692 | } 693 | else if( remappedMaterialNamesMustMatch ) 694 | remappedMaterial = GetMaterialWithName( duplicateMaterialsLookup, name ); 695 | } 696 | 697 | if( remappedMaterial && remappedMaterial != material ) 698 | { 699 | data.materialExtractModes.Add( ExtractMode.Remap ); 700 | data.remappedMaterials.Add( remappedMaterial ); 701 | } 702 | else 703 | #endif 704 | { 705 | data.materialExtractModes.Add( materialAlreadyExtracted ? ExtractMode.Ignore : ExtractMode.Extract ); 706 | data.remappedMaterials.Add( null ); 707 | 708 | #if UNITY_2019_1_OR_NEWER 709 | duplicateMaterials.Add( material ); 710 | #endif 711 | } 712 | } 713 | } 714 | } 715 | 716 | #if UNITY_2019_1_OR_NEWER 717 | if( dontRemapMaterialsAcrossDifferentModels ) 718 | duplicateMaterialsLookup.Clear(); 719 | #endif 720 | } 721 | } 722 | 723 | #if UNITY_2019_1_OR_NEWER 724 | private HashSet GetMaterialsWithCRC( Dictionary> lookup, Material material ) 725 | { 726 | int crcHash = material.ComputeCRC(); 727 | HashSet result; 728 | if( !lookup.TryGetValue( crcHash, out result ) ) 729 | lookup[crcHash] = result = new HashSet(); 730 | 731 | return result; 732 | } 733 | 734 | private Material GetMaterialWithName( Dictionary> lookup, string name ) 735 | { 736 | foreach( HashSet allMaterials in lookup.Values ) 737 | { 738 | foreach( Material material in allMaterials ) 739 | { 740 | if( material.name == name ) 741 | return material; 742 | } 743 | } 744 | 745 | return null; 746 | } 747 | #endif 748 | 749 | private void ExtractMaterials() 750 | { 751 | if( materialsFolder.EndsWith( "/" ) ) 752 | materialsFolder = materialsFolder.Substring( 0, materialsFolder.Length - 1 ); 753 | 754 | if( !Directory.Exists( materialsFolder ) ) 755 | { 756 | Directory.CreateDirectory( materialsFolder ); 757 | AssetDatabase.ImportAsset( materialsFolder, ImportAssetOptions.ForceUpdate ); 758 | } 759 | 760 | List dirtyModelImporters = new List( modelData.Count ); 761 | Dictionary extractedMaterials = new Dictionary( modelData.Count * 8 ); 762 | 763 | for( int i = 0; i < modelData.Count; i++ ) 764 | { 765 | ExtractData data = modelData[i]; 766 | if( !data.model ) 767 | continue; 768 | 769 | AssetImporter modelImporter = AssetImporter.GetAtPath( AssetDatabase.GetAssetPath( data.model ) ); 770 | 771 | // Remap/extract the model's materials 772 | // Credit: https://forum.unity.com/threads/batch-change-all-fbx-default-materials-help.626341/#post-6530939 773 | using( SerializedObject so = new SerializedObject( modelImporter ) ) 774 | { 775 | SerializedProperty materials = so.FindProperty( "m_Materials" ); 776 | SerializedProperty externalObjects = so.FindProperty( "m_ExternalObjects" ); 777 | 778 | for( int materialIndex = 0; materialIndex < materials.arraySize; materialIndex++ ) 779 | { 780 | SerializedProperty id = materials.GetArrayElementAtIndex( materialIndex ); 781 | string name = id.FindPropertyRelative( "name" ).stringValue; 782 | string type = id.FindPropertyRelative( "type" ).stringValue; 783 | 784 | // j: index of the target material in data's lists 785 | int j = ( materialIndex < data.materialNames.Count && data.materialNames[materialIndex] == name ) ? materialIndex : data.materialNames.IndexOf( name ); 786 | if( j < 0 ) 787 | { 788 | // This can only occur if user reimports the model with more materials when 'inModelSelectionPhase' is false 789 | Debug.LogWarning( data.model.name + "." + name + " material has no matching data, skipped", data.model ); 790 | continue; 791 | } 792 | 793 | Material targetMaterial = null; 794 | switch( data.materialExtractModes[j] ) 795 | { 796 | case ExtractMode.Extract: 797 | { 798 | if( data.originalMaterials[j] && !AssetDatabase.IsMainAsset( data.originalMaterials[j] ) ) 799 | targetMaterial = data.originalMaterials[j]; 800 | else 801 | Debug.LogWarning( data.model.name + "." + name + " isn't extracted because either the material doesn't exist or it is already extracted", data.model ); 802 | 803 | break; 804 | } 805 | case ExtractMode.Remap: 806 | { 807 | if( data.remappedMaterials[j] && ( data.originalMaterials[j] != data.remappedMaterials[j] || !AssetDatabase.IsMainAsset( data.remappedMaterials[j] ) ) ) 808 | targetMaterial = data.remappedMaterials[j]; 809 | else 810 | Debug.LogWarning( data.model.name + "." + name + " isn't remapped because either the material doesn't exist or it is already extracted", data.model ); 811 | 812 | break; 813 | } 814 | } 815 | 816 | if( !targetMaterial ) 817 | continue; 818 | else if( !AssetDatabase.IsMainAsset( targetMaterial ) ) 819 | { 820 | Material extractedMaterial; 821 | if( !extractedMaterials.TryGetValue( targetMaterial, out extractedMaterial ) ) 822 | { 823 | extractedMaterials[targetMaterial] = extractedMaterial = new Material( targetMaterial ); 824 | AssetDatabase.CreateAsset( extractedMaterial, AssetDatabase.GenerateUniqueAssetPath( materialsFolder + "/" + targetMaterial.name + ".mat" ) ); 825 | } 826 | 827 | targetMaterial = extractedMaterial; 828 | } 829 | 830 | SerializedProperty materialProperty = null; 831 | for( int externalObjectIndex = 0; externalObjectIndex < externalObjects.arraySize; externalObjectIndex++ ) 832 | { 833 | SerializedProperty pair = externalObjects.GetArrayElementAtIndex( externalObjectIndex ); 834 | string externalName = pair.FindPropertyRelative( "first.name" ).stringValue; 835 | string externalType = pair.FindPropertyRelative( "first.type" ).stringValue; 836 | 837 | if( externalType == type && externalName == name ) 838 | { 839 | materialProperty = pair.FindPropertyRelative( "second" ); 840 | break; 841 | } 842 | } 843 | 844 | if( materialProperty == null ) 845 | { 846 | SerializedProperty currentSerializedProperty = externalObjects.GetArrayElementAtIndex( externalObjects.arraySize++ ); 847 | currentSerializedProperty.FindPropertyRelative( "first.name" ).stringValue = name; 848 | currentSerializedProperty.FindPropertyRelative( "first.type" ).stringValue = type; 849 | currentSerializedProperty.FindPropertyRelative( "first.assembly" ).stringValue = id.FindPropertyRelative( "assembly" ).stringValue; 850 | currentSerializedProperty.FindPropertyRelative( "second" ).objectReferenceValue = targetMaterial; 851 | } 852 | else 853 | materialProperty.objectReferenceValue = targetMaterial; 854 | } 855 | 856 | if( so.hasModifiedProperties ) 857 | { 858 | dirtyModelImporters.Add( modelImporter ); 859 | so.ApplyModifiedPropertiesWithoutUndo(); 860 | } 861 | } 862 | } 863 | 864 | for( int i = 0; i < dirtyModelImporters.Count; i++ ) 865 | dirtyModelImporters[i].SaveAndReimport(); 866 | } 867 | } 868 | } --------------------------------------------------------------------------------