Skip to content

Instantly share code, notes, and snippets.

@RH2
Last active February 25, 2025 02:33
Show Gist options
  • Select an option

  • Save RH2/9cbd6907e8eb7909d6a8112f05323343 to your computer and use it in GitHub Desktop.

Select an option

Save RH2/9cbd6907e8eb7909d6a8112f05323343 to your computer and use it in GitHub Desktop.
LOSPEC.COM .HEX MATERIAL ADDON
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