Last active
February 25, 2025 02:33
-
-
Save RH2/9cbd6907e8eb7909d6a8112f05323343 to your computer and use it in GitHub Desktop.
LOSPEC.COM .HEX MATERIAL ADDON
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| bl_info = { | |
| "name": "Hex Color Materials from URL with Color Atlas", | |
| "author": "RH", | |
| "version": (1, 3), | |
| "blender": (3, 0, 0), | |
| "location": "View3D > Sidebar > Hex Materials Tab", | |
| "description": "Create materials and color atlas from hex codes in a URL text file", | |
| "category": "Material", | |
| } | |
| import bpy | |
| import urllib.request | |
| import math | |
| import re | |
| from bpy.props import StringProperty, IntProperty, BoolProperty, EnumProperty | |
| # Preset URL list | |
| PRESET_URLS = [ | |
| ("custom", "Custom URL", "Use a custom URL"), | |
| ("https://lospec.com/palette-list/endesga-32.hex", "Endesga 32", "32-color palette by Endesga"), | |
| ("https://lospec.com/palette-list/kirokaze-gameboy.hex", "Kirokaze GameBoy", "GameBoy inspired palette"), | |
| ("https://lospec.com/palette-list/blk-nx64.hex", "BLK NX64", "64-color palette"), | |
| ("https://lospec.com/palette-list/peregrine16.hex", "Peregrine 16", "16-color palette"), | |
| ("https://lospec.com/palette-list/cc-29.hex", "CC-29", "29-color palette"), | |
| ("https://lospec.com/palette-list/dungeon-20.hex","Dungeon","20-colors"), | |
| ("https://lospec.com/palette-list/aero16.hex","Aero16","16-colors") | |
| ] | |
| class HEX_MATERIALS_Properties(bpy.types.PropertyGroup): | |
| url_preset: EnumProperty( | |
| name="URL Presets", | |
| description="Choose from preset color palette URLs", | |
| items=PRESET_URLS, | |
| default="custom" | |
| ) | |
| url: StringProperty( | |
| name="Custom URL", | |
| description="URL to text file with hex codes (one per line)", | |
| default="" | |
| ) | |
| grid_size: IntProperty( | |
| name="Grid Size", | |
| description="Number of colors per row in the atlas", | |
| default=8, | |
| min=1, | |
| max=32 | |
| ) | |
| pixel_size: IntProperty( | |
| name="Pixel Size", | |
| description="Size in pixels of each color square in the atlas", | |
| default=32, | |
| min=8, | |
| max=256 | |
| ) | |
| class HEX_MATERIALS_OT_FetchColors(bpy.types.Operator): | |
| """Fetch and process hex colors from URL""" | |
| bl_idname = "material.fetch_hex_colors" | |
| bl_label = "Fetch Colors" | |
| bl_options = {'INTERNAL'} | |
| def execute(self, context): | |
| props = context.scene.hex_materials_props | |
| # Determine which URL to use (preset or custom) | |
| if props.url_preset == "custom": | |
| url = props.url | |
| else: | |
| url = props.url_preset | |
| if not url: | |
| self.report({'ERROR'}, "No URL provided") | |
| return {'CANCELLED'} | |
| try: | |
| # Read the file from URL | |
| response = urllib.request.urlopen(url) | |
| data = response.read().decode('utf-8') | |
| # Split by newline | |
| hex_codes = data.strip().split('\n') | |
| # Filter out empty lines | |
| hex_codes = [code.strip() for code in hex_codes if code.strip()] | |
| # Process the hex codes | |
| valid_colors = [] | |
| for hex_code in hex_codes: | |
| if hex_code.startswith('#'): | |
| hex_code = hex_code[1:] | |
| if len(hex_code) == 6: | |
| try: | |
| # Convert hex to RGB | |
| r = int(hex_code[0:2], 16) / 255.0 | |
| g = int(hex_code[2:4], 16) / 255.0 | |
| b = int(hex_code[4:6], 16) / 255.0 | |
| valid_colors.append((hex_code, (r, g, b))) | |
| except ValueError: | |
| self.report({'WARNING'}, f"Invalid hex code: {hex_code}") | |
| continue | |
| else: | |
| self.report({'WARNING'}, f"Invalid hex code length: {hex_code}") | |
| continue | |
| if not valid_colors: | |
| self.report({'ERROR'}, "No valid colors found in the file") | |
| return {'CANCELLED'} | |
| # Store colors in scene property for use by other operators | |
| context.scene['hex_colors'] = valid_colors | |
| # Get palette name | |
| palette_name = "Custom" | |
| for item in PRESET_URLS: | |
| if item[0] == url: | |
| palette_name = item[1] | |
| break | |
| # If using custom URL, try to extract name from URL | |
| if palette_name == "Custom" and props.url_preset == "custom": | |
| match = re.search(r'palette-list/([^.]+)', url) | |
| if match: | |
| palette_name = match.group(1).replace('-', ' ').title() | |
| context.scene['palette_name'] = palette_name | |
| self.report({'INFO'}, f"Successfully fetched {len(valid_colors)} colors from {palette_name}") | |
| return {'FINISHED'} | |
| except Exception as e: | |
| self.report({'ERROR'}, f"Error: {str(e)}") | |
| return {'CANCELLED'} | |
| class HEX_MATERIALS_OT_CreateAtlas(bpy.types.Operator): | |
| """Create color atlas from fetched hex codes""" | |
| bl_idname = "material.create_color_atlas" | |
| bl_label = "Create Color Atlas" | |
| bl_options = {'REGISTER', 'UNDO'} | |
| def execute(self, context): | |
| if 'hex_colors' not in context.scene or not context.scene['hex_colors']: | |
| # Run the fetch operation first | |
| bpy.ops.material.fetch_hex_colors() | |
| if 'hex_colors' not in context.scene or not context.scene['hex_colors']: | |
| self.report({'ERROR'}, "Failed to fetch colors. Please check the URL") | |
| return {'CANCELLED'} | |
| # Get colors and palette name | |
| colors = context.scene['hex_colors'] | |
| palette_name = context.scene.get('palette_name', "Custom") | |
| # Create the atlas | |
| img = self.create_color_atlas(context, colors, palette_name) | |
| self.report({'INFO'}, f"Created color atlas '{img.name}' with {len(colors)} colors") | |
| return {'FINISHED'} | |
| def create_color_atlas(self, context, colors, palette_name): | |
| props = context.scene.hex_materials_props | |
| grid_size = props.grid_size | |
| pixel_size = props.pixel_size | |
| # Calculate grid dimensions | |
| total_colors = len(colors) | |
| rows = math.ceil(total_colors / grid_size) | |
| # Create a new image | |
| img_width = grid_size * pixel_size | |
| img_height = rows * pixel_size | |
| # Format the atlas name based on palette | |
| atlas_name = f"Atlas_{palette_name.replace(' ', '_')}" | |
| # Check if atlas already exists and remove it | |
| if atlas_name in bpy.data.images: | |
| bpy.data.images.remove(bpy.data.images[atlas_name]) | |
| # Create new image | |
| img = bpy.data.images.new( | |
| name=atlas_name, | |
| width=img_width, | |
| height=img_height, | |
| alpha=True | |
| ) | |
| # Initialize pixels | |
| pixels = [0] * (img_width * img_height * 4) | |
| # Fill the image with color squares | |
| for i, (hex_code, color) in enumerate(colors): | |
| if i >= total_colors: | |
| break | |
| # Calculate position in the grid | |
| col = i % grid_size | |
| row = i // grid_size | |
| # Calculate pixel bounds | |
| start_x = col * pixel_size | |
| start_y = row * pixel_size | |
| # Fill the square | |
| for y in range(start_y, start_y + pixel_size): | |
| for x in range(start_x, start_x + pixel_size): | |
| # Calculate pixel index | |
| idx = (y * img_width + x) * 4 | |
| # Set RGBA values (R, G, B, A) | |
| pixels[idx] = color[0] # R | |
| pixels[idx + 1] = color[1] # G | |
| pixels[idx + 2] = color[2] # B | |
| pixels[idx + 3] = 1.0 # A | |
| # Update the image with pixel data | |
| img.pixels = pixels | |
| img.update() | |
| # Pack the image into the .blend file | |
| img.pack() | |
| return img | |
| class HEX_MATERIALS_OT_CreateMaterials(bpy.types.Operator): | |
| """Create materials from fetched hex codes""" | |
| bl_idname = "material.create_hex_materials" | |
| bl_label = "Create Materials" | |
| bl_options = {'REGISTER', 'UNDO'} | |
| def execute(self, context): | |
| if 'hex_colors' not in context.scene or not context.scene['hex_colors']: | |
| # Run the fetch operation first | |
| bpy.ops.material.fetch_hex_colors() | |
| if 'hex_colors' not in context.scene or not context.scene['hex_colors']: | |
| self.report({'ERROR'}, "Failed to fetch colors. Please check the URL") | |
| return {'CANCELLED'} | |
| # Get colors and palette name | |
| colors = context.scene['hex_colors'] | |
| palette_name = context.scene.get('palette_name', "Custom") | |
| # Create materials | |
| created_count = 0 | |
| for hex_code, color in colors: | |
| # Create new material | |
| mat_name = f"{palette_name}_{hex_code}" | |
| mat = bpy.data.materials.new(name=mat_name) | |
| mat.use_nodes = True | |
| # Set BSDF color | |
| bsdf = mat.node_tree.nodes.get('Principled BSDF') | |
| if bsdf: | |
| bsdf.inputs[0].default_value = (color[0], color[1], color[2], 1.0) | |
| created_count += 1 | |
| self.report({'INFO'}, f"Created {created_count} materials from {palette_name} palette") | |
| return {'FINISHED'} | |
| class HEX_MATERIALS_PT_panel(bpy.types.Panel): | |
| """Hex Materials Panel""" | |
| bl_label = "Hex Materials from URL" | |
| bl_idname = "HEX_MATERIALS_PT_panel" | |
| bl_space_type = 'VIEW_3D' | |
| bl_region_type = 'UI' | |
| bl_category = 'Hex Materials' | |
| def draw(self, context): | |
| layout = self.layout | |
| props = context.scene.hex_materials_props | |
| row = layout.row() | |
| row.label(text="Load hex colors from URL:") | |
| box = layout.box() | |
| col = box.column(align=True) | |
| # URL selection | |
| col.prop(props, "url_preset", text="") | |
| # Only show custom URL input if "Custom URL" is selected | |
| if props.url_preset == "custom": | |
| col.prop(props, "url", text="") | |
| # Fetch button | |
| col.operator("material.fetch_hex_colors", text="Fetch Colors", icon='FILE_REFRESH') | |
| # Atlas options | |
| box.label(text="Atlas Settings:") | |
| row = box.row() | |
| row.prop(props, "grid_size") | |
| row.prop(props, "pixel_size") | |
| # Create buttons - side by side | |
| row = layout.row(align=True) | |
| row.operator("material.create_color_atlas", text="Create Atlas", icon='TEXTURE') | |
| row.operator("material.create_hex_materials", text="Create Materials", icon='MATERIAL') | |
| # Show current palette if available | |
| if 'palette_name' in context.scene and 'hex_colors' in context.scene: | |
| palette_name = context.scene.get('palette_name', "") | |
| color_count = len(context.scene.get('hex_colors', [])) | |
| layout.label(text=f"Current palette: {palette_name} ({color_count} colors)") | |
| # Registration | |
| classes = ( | |
| HEX_MATERIALS_Properties, | |
| HEX_MATERIALS_OT_FetchColors, | |
| HEX_MATERIALS_OT_CreateAtlas, | |
| HEX_MATERIALS_OT_CreateMaterials, | |
| HEX_MATERIALS_PT_panel, | |
| ) | |
| def register(): | |
| for cls in classes: | |
| bpy.utils.register_class(cls) | |
| bpy.types.Scene.hex_materials_props = bpy.props.PointerProperty(type=HEX_MATERIALS_Properties) | |
| def unregister(): | |
| for cls in classes: | |
| bpy.utils.unregister_class(cls) | |
| del bpy.types.Scene.hex_materials_props | |
| if __name__ == "__main__": | |
| register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment