Skip to content

Instantly share code, notes, and snippets.

@michaelgold
Created February 10, 2023 22:18
Show Gist options
  • Save michaelgold/8784c0bd0fc0d9e938da923b4d320dd9 to your computer and use it in GitHub Desktop.
Save michaelgold/8784c0bd0fc0d9e938da923b4d320dd9 to your computer and use it in GitHub Desktop.
Blender script to decode embedded generative art content from a JPEG file inscribed in an ordinal
# ▓▓░ ░▓ ░▓░
# ░██ ▓█ ▓▓ ░▓ ░▓████▓ ░▓ ░▓▓
# ██▓ ▓█▓ █▓ ░█░ ▓██▓░░█▓ █▓ ▓█
# ▓██░ ▓██ ▓▓ ▓█░ ▓▓ ▓█▓░ ▓█░ ░█░ ░█▓
# ░██▓ ▓██▓ ░██░ ▓▓ ░█░ ▓█▓ ░█▓ ▓▓ ▓█░
# ░█▓█▓ ░██▓ ░█▓ ░█░ ▓█ ▓█▓ ░█▓ ░█░ █▓
# ▓█▓█ ░█▓█▓ ▓▓ █▓ ▓█▓ ░█▓ ▓▓ ░█░
# ▓█░▓▓░▓▓▓█ ░ █▓ ░ ░█ ░█▓ ░█▓ ░ █▓ ░▓▓
# ▓█▓ █▓▓▓░█▓ ▓▓ ▓██▓ ░█░▓█▓ ░▓██▓ ░███░ ▓▓ █▓ ░ ▓██▓ ░█ ░▓███
# ░█▓ ▓██▓ ▓█░ ░█░ ▓▓ ▓▓ ▓█▓██▓ ▓█▓░░ ▓█░ █░ ░█░ ░█░ ░▓░▓▓░██▓░░▓▓ ▓▓░░▓▓
# ░█▓ ▓██░ ▓▓ ▓▓ ▓▓ ▓░░██░░█░ ▓█░ ▓░ ░█░ ▓▓ ▓▓ ▓▓ ██▓░█ ▓█▓▓▓█░ ▓▓ ░█▓
# ▓▓ ░██░ ░█▓ █░ ░█ ▓█▓ ▓▓ █░ ░█ ▓▓ ▓▓ █▓ █░ ▓█▓ ▓▓ █░ ▓▓ ░█ ▓█░
# ▓█░ ▓█▓ ░█▓ ░▓ ▓▓ █▓ █░ ▓█ ▓▓ ▓█▓░ ░█░ █░ ▓██ ▓▓ ▓▓ █▓ ░▓ ▓▓█░
# ▓████░ ░█▓ ▓███▓▓▓▓▓███████░ ░███████████▓████████▓▓ ▓▓░░▓▓██▓ ▓▓█▓ ██▓▓▓█▓█▓ ▓███▓
# ░▓▓▓▓▓▓ ░░ ░░░ ░░░ ░░░░░░ ░░░░░░░ ░ ░░░░░░░░░░ ░███▓▓▓█ ░░ ░░░░░░░░ ░░░
# ░░ ▓▓
# ▓█
# ░█▓
# ▓▓
# ▓▓
# This script is part of the Shaderverse project. It is licensed under the MIT license.
# Author: @michaelgold
# Last Updataed: 2022-02-10
# For a tutorial see: https://twitter.com/michaelgold/status/1622664031205990403
# Instructions:
# 1. Open Blender
# 2. Click the Scripting tab at the top of the window
# 3. Select Text > New from the menut to create a new text file
# 4. Copy and paste the contents of this file into the new text file
# 5. Select Text > Run Script from the menu to run the script
# 6. Enter a seed value (each seed value will produce a different result)
# 7. Select a jpeg file that contains embedded generative content
# - some files to try:
# - seed 1000000, https://ordinals.com/inscription/e4dc672b6ad86c8587b921ec24a070243df61721b07e0ae80b122489c579b9a0i0
# - seed 1000001, https://ordinals.com/inscription/7ed1598584f72d85294e8a001e8a7fb92fc0000e9bb2ffeec518e1f5bea258f2i0
# - seed 1000010, https://ordinals.com/inscription/ffc2c418b4f5c9b88cc2cb842dc07ad1014e168f7ceb266ee9bf52c0a7d38322i0
# - seed 1000011, https://ordinals.com/inscription/6dedd63b944b56d045c488cd35f84c0be855c5d74c5a6c79cc07dbd9ac29e959i0
# 8. Try different seed values and different images to see what happens :)
import bpy
from bpy_extras.io_utils import ImportHelper
from bpy.props import StringProperty, BoolProperty, IntProperty
from bpy.types import Operator, PropertyGroup
from pathlib import Path
import tempfile
import os
class OT_MessageBox(bpy.types.Operator):
bl_idname = "gold.messagebox"
bl_label = ""
message: StringProperty(
name = "message",
description = "message",
default = ''
)
line2: StringProperty(
name = "message",
description = "message",
default = ''
)
def execute(self, context):
self.report({'INFO'}, self.message)
print(self.message)
return {'FINISHED'}
def invoke(self, context, event):
return context.window_manager.invoke_props_dialog(self, width = 400)
def draw(self, context):
self.layout.label(text=self.message)
self.layout.label(text=self.line2)
class OT_Form(bpy.types.Operator):
"""Display a form that prompts the user to enter a seed and select a jpeg file"""
bl_idname = "gold.form"
bl_label = ""
seed: IntProperty(
name="Seed",
description="Seed",
default=100000
)
path: StringProperty(
name="JPEG",
description="Path to Directory",
default="",
maxlen=1024)
def remove_all_objects(self, context):
"""Remove all objects from scene"""
for obj in bpy.data.objects:
bpy.data.objects.remove(obj, do_unlink=True)
def find_geometry_nodes(self, object_ref: bpy.types.Object):
"""Find all geometry nodes in an object"""
geometry_node_objects = []
object_name = object_ref.name
try:
object_modifiers = object_ref.modifiers.items()
except AttributeError as error:
raise Exception(f"{error}: for {object_name}")
for modifier in object_modifiers:
modifier_name = modifier[0]
modifier_ref = modifier[1]
if hasattr(modifier_ref, "node_group"):
node_group = modifier_ref.node_group
try:
if node_group.type == "GEOMETRY":
node_object = {
"object_name": object_name,
"object_ref": object_ref,
"modifier_name": modifier_name,
"modifier_ref": modifier_ref,
"node_group": node_group
}
geometry_node_objects.append(node_object)
except AttributeError as error:
raise Exception(f"{error}: Could not find a Node Group type in object: {object_name}. Did you add an empty geometry node modifier?")
return geometry_node_objects
def update_mesh(self, node_object):
"""Update the mesh of a geometry node object"""
self.set_node_inputs_from_seed(node_object)
object_name = node_object["object_name"]
object_ref = bpy.data.objects[object_name]
mesh_name = object_ref.data.name
mesh = bpy.data.meshes[mesh_name]
mesh.update()
def set_node_inputs_from_seed(self, node_object):
modifier = node_object["modifier_ref"]
node_group = modifier.node_group
object_name = node_object["object_name"]
if "seed" in node_group.inputs.keys():
item_ref = node_group.inputs["seed"]
item_input_id = item_ref.identifier
modifier[item_input_id] = int(self.seed)
def update_geonodes(self, context):
"""find all geonodes then update the node object based on the generated metadata"""
geometry_node_objects = []
# find all geonode objects
print("Finding all geometry nodes...")
for object_ref in bpy.data.objects.values():
if object_ref is not None:
geometry_node_objects += self.find_geometry_nodes(object_ref)
print("Found {} geometry nodes".format(len(geometry_node_objects)))
# update all geonodes with metadata
for node_object in geometry_node_objects:
self.update_mesh(node_object)
print("Updated all geometry nodes")
def set_default_render_settings(self, context):
"""Set default render settings"""
bpy.data.scenes["Scene"].render.engine = 'CYCLES'
bpy.data.scenes["Scene"].cycles.device = 'GPU'
bpy.data.scenes["Scene"].cycles.preview_samples = 16
bpy.data.scenes["Scene"].cycles.use_preview_denoising = True
bpy.data.scenes["Scene"].cycles.samples = 64
bpy.data.scenes["Scene"].render.resolution_x = 1280
bpy.data.scenes["Scene"].render.resolution_y = 1280
def set_camera_view(self, context):
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
area.spaces[0].overlay.show_overlays = False
area.spaces[0].shading.type = 'RENDERED'
override = bpy.context.copy()
override['area'] = area
bpy.ops.view3d.view_camera(override)
def execute(self, context):
"""decode the blender data from the jpeg file and add it to the scene"""
self.remove_all_objects(context)
input_file = self.path
if input_file == "":
self.report({'ERROR'}, "No file selected")
return {'CANCELLED'}
with open(input_file, "rb") as input:
data = input.read()
start_pos = data.find(b"Blender File")
if start_pos != -1:
input.seek(start_pos + len(b"Blender File"))
temp_dir = tempfile.mkdtemp(suffix=None, prefix=None, dir=None)
blend_file = Path(temp_dir, "output.blend")
with open(blend_file, "wb") as blend:
blend.write(input.read())
with bpy.data.libraries.load(blend.name) as (data_from, data_to):
data_to.objects = data_from.objects
for obj in data_to.objects:
if obj is not None:
bpy.context.scene.collection.objects.link(obj)
for obj in data_from.objects:
if obj is not None:
if obj.hide_render:
bpy.data.objects[obj.name].hide_render = True
bpy.data.objects[obj.name].hide_set(True)
else:
bpy.ops.gold.messagebox('INVOKE_DEFAULT', message="No Blender Data Found.", line2="Are you sure this is a JPEG with encoded generator content?")
self.update_geonodes(context)
self.set_default_render_settings(context)
self.set_camera_view(context)
return {'FINISHED'}
def invoke(self, context, event):
"""Invoke the form"""
return context.window_manager.invoke_props_dialog(self, width = 400)
def draw(self, context):
"""Draw the form fields"""
self.layout.label(text="Enter the seed and path to the jpeg file.")
self.layout.label(text="")
self.layout.prop(self, "seed", text="Seed")
row = self.layout.row()
row.prop(self, "path", text="JPEG")
browse = row.operator(OT_SelectFIle.bl_idname, text="", icon='FILE_FOLDER')
browse.seed = self.seed
class OT_SelectFIle(Operator, ImportHelper):
seed: IntProperty()
filter_glob: StringProperty(default='*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp', options={'HIDDEN'})
bl_idname = "gold.select_file"
bl_label = "Browse"
def execute(self, context):
"""Run the form again with the new file path"""
bpy.ops.gold.form('INVOKE_DEFAULT', seed=self.seed, path=self.filepath)
return {'FINISHED'}
classes = [OT_SelectFIle, OT_MessageBox, OT_Form]
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
if __name__ == "__main__":
register()
bpy.ops.gold.form('INVOKE_DEFAULT')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment