DEV Community ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป

Cover image for Create NFT 3D Collectibles Using Blender Scripting
hideckies
hideckies

Posted on • Updated on • Originally published at blog.hdks.org

Create NFT 3D Collectibles Using Blender Scripting

I wanted to create 3D characters for about 1000+ NFT Collectibles, but didn't know how to do that.

It's stupid to create all models one by one manually.

Actually, I like to use Blender for creating 3D models so also want to use it regarding to that.

When I searched on the internet, I found the solution that Blender Scripting (Python) can be used to randomly combine a variety of traits to create 3D models programatically.

It uses Python language.

GitHub Repo

A demo project is available.

Check the GitHub Repogitory: https://github.com/hideckies/nft-collectibles-blender-python

0. Prerequisite

  • Blender installed on your PC.

1. Create Directories

The directory tree is as follows.

Tree

  • parts --- Each 3D part included in this.
  • body --- Bodies.
  • head --- Heads.
  • misc --- Other file (background, lights, camera, etc.).
  • outputs --- Images & Metadata generated by Blender Scripting.
  • scripts --- Script files.

2. Create Each Part on Blender Manually

This time, create 2 types of parts including the head, body. And misc (a file included background image, camera, light).

Steps:

  1. Open Blender and create a new file and remove all default objects, and save it in body or head folder. (e.g. nft-collectibles/parts/head/head_rabbit.blend)
  2. In Blender, create a new collection named same as the file name. (e.g. If the file name is head_rabbit.blend, the collection name is head_rabbit)
  3. Create a part model in the collection which be created just now.
  4. Adjust the size and position to make the character look natural.
  5. Save finally.

*This article does not explain how to modeling.

For example (Heads):

File name: nft-collectibles/parts/head/head_rabbit.blend

Collection name: head_rabbit

Head_1

File name: nft-collectibles/parts/head/head_frog.blend

Collection name: head_frog

Head_2

For example (Bodies):

File name: nft-collectibles/parts/body/body_shirt.blend

Collection name: body_shirt

Body_1

File name: nft-collectibles/parts/head/body_zombie.blend

Collection name: body_zombie

Body_2

For example (Misc):

File name: nft-collectibles/parts/misc/misc.blend

Collection name: misc

misc

3. Create Script Files for Scripting

To create script files, open your favorite code /text editor or a text editor in the Scripting Workspace on Blender.

3-1. gen_metadata.py

This is for generating random metadata for NFT.

import bpy  # The module for Blender Python
import json
import random

# This time, 100 characters will be generated.
TOTAL_CHARACTERS = 100

# Specify the directory in which generate images finally.
OUTPUTS_DIR = "c:/nft-collectibles/outputs/"

# List of parts (the file name (collection name) of each created part is included in this.)
list_body = [
    "hoodie",
    "overall",
    "pants",
    "shirt",
    "tanktop",
    "t_shirt",
    "zombie"
]
list_head = [
    "devil",
    "dragon",
    "frog",
    "king",
    "pirate",
    "rabbit",
    "robot",
    "skull",
    "warrior"
]


def random_attributes():
    # Select parts randomly from lists
    random_head = random.choice(list_head)
    random_body = random.choice(list_body)

    # Format each name for the value of attributes in metadata (e.g. "head_rabbit" -> "Head Rabbit")
    random_body = random_body.replace("_", " ").title()
    random_head = random_head.replace("_", " ").title()

    attributes = [
        {
            "trait_type": "Body",
            "value": random_body
        },
        {
            "trait_type": "Head",
            "value": random_head
        }
    ]

    return attributes


def main():
    print("Start generating metadata...")

    # Create a tempolary dict
    dict_list = []

    for i in range(TOTAL_CHARACTERS):
        attributes = random_attributes(i)
        d = { "attributes": attributes }
        dict_list.append(d)

    # Create a list of unique data removed duplicates 
    unique_list = list(map(json.loads, set(map(json.dumps, dict_list))))
    # If the duplicated found, cancel scripting and try again
    if len(unique_list) < TOTAL_CHARACTERS:
        print("ERROR: Properties duplicate")
        return

    # Save metadata files (.json) to outputs folder
    for i, attr_dict in enumerate(unique_list):
        # Create a metadata object
        obj = {
            "name": "Character #" + str(i),
            "description": "A collection of 3D character",
            "image": "https://example.com/"+ str(i) + ".png",
            "external_url": "https://example.com/",
            "attributes": attr_dict["attributes"]
        }
        with open(OUTPUTS_DIR + str(i) + ".json", 'w') as outjson:
            json.dump(obj, outjson, indent=4)

        print("Generated metadata id: {}\n".format(i))


main()
Enter fullscreen mode Exit fullscreen mode

3-2. gen_model.py

This is for generating models from metadata.

import bpy
import json
import random
import shutil

# This time, 100 characters will be generated.
TOTAL_CHARACTERS = 100

# Path
PARTS_DIR = "c:/nft-collectibles/parts/"
OUTPUTS_DIR = "c:/nft-collectibles/outputs/"


# Initialize the scene (deleting the all default objects)
def init():
    for obj in bpy.data.objects:
        bpy.data.objects.remove(obj)
    for col in bpy.data.collections:
        bpy.data.collections.remove(col)


# Set configurations for rendering
def set_render_config():

    # For example, set the rendering engine "EEVEE" (but Blender may be configured EEVEE default. if so, you don't need to do that)
    r = bpy.context.scene.render
    r.engine = "BLENDER_EEVEE"
    r.resolution_x = 1024
    r.resolution_y = 1024

    # Set the output file format
    r.image_settings.file_format = 'PNG'


def append_misc():
    path = PARTS_DIR + "misc/" + "misc.blend/Collection/"
    collection_name = "misc"
    bpy.ops.wm.append(filename=collection_name, directory=path)

    # Link camera to scene
    cam = bpy.data.objects["camera"]
    scene = bpy.context.scene
    scene.camera = cam


def append_body(trait_value):
    body_name = "body_" + trait_value

    path = PARTS_DIR + "body/" + body_name + ".blend/Collection/"
    bpy.ops.wm.append(filename=body_name, directory=path)


def append_head(trait_value):
    head_name = "head_" + trait_value

    path = PARTS_DIR + "head/" + head_name + ".blend/Collection/"
    bpy.ops.wm.append(filename=head_name, directory=path)


def render(id):
    # Render
    bpy.ops.render.render(write_still=1)

    # Save
    bpy.data.images['Render Result'].save_render(filepath=OUTPUTS_DIR + id + ".png")


def remove_parts():
    # Remove all parts except "misc"
    for col in bpy.data.collections:
        if col.name != "misc":
            for obj in col.objects:
                bpy.data.objects.remove(obj)
            bpy.data.collections.remove(col)
            continue


def generate(id, metadata):
    for attr in metadata["attributes"]:
        # Body
        if attr["trait_type"] == "Body" and attr["value"] != "":
            append_body(attr["value"])
        # Head
        if attr["trait_type"] == "Head" and attr["value"] != "":
            append_head(attr["value"])

    render(str(id))

    # After rendered, remove all parts
    remove_parts()


def main():
    print("Start generating models...")

    init()
    set_render_config()
    append_misc()   # Regarding "misc", add it to the scene in advance because it is common to all characters.

    # Generate character images
    for i in TOTAL_CHARACTERS:
        with open(OUTPUTS_DIR + str(i) + ".json", 'r') as metaJson:
            data = json.load(metaJson)
            generate(i, data)
            print("Generated model id: {}\n".format(id))

main()
Enter fullscreen mode Exit fullscreen mode

4. Run Script

After creating script files, you can run them.

Open Blender and click the Scripting workspace next to Geometry Nodes on the top of the editor.

Scripting workspace

To check the status during processing, you can open the console by clicking "Window"-> "Toggle System Console" in the top menu.

4-1. Generate metadata

Click the Open -> choose a gen_metadata.py -> click the Run Script.

As the above source code shows (gen_metadata.py, line 68-73), If the combination of data is duplicated, the process will be canceled.

In that case, you need to click the Run Script again.

After that, you can see that the metadata files such as 0.json, 1.json are generated in the outputs folder.

4-2. Generate characters

Click the Open -> choose a gen_model.py -> click the Run Script.

After that, you can see that the rendered image files such as 0.png, 1.png are generated in the outputs folder.

Complete

Top comments (19)

Collapse
 
thankgod_jacob_3ec2e40787 profile image
ThankGod Jacob

Hello, can I edit and make use of your NFT example as the base model for my own NFT collection?

Collapse
 
hideckies profile image
hideckies

Hello. Sure, feel free to use it.

Collapse
 
yojirasuro profile image
Yoji Rasuro

Hi there! Thank you so much for the script, it's seems great!
However, I don't know how to use it. -_-"
I tried using this with the files you provided but I keep getting snagged at the duplicate error. I don't understand what I should be doing to go past that. :(
What duplicates is it talking about?

Collapse
 
hideckies profile image
hideckies

Hi, thanks!

As I said here, please set TOTAL_CHARACTERS = 2. I think it will probably work. Anyway, repeat "Run" until you succeed.

This error occurs when the combination of parts is duplicated.
Our ideal is to create a unique collection, so we have to avoid the exact same characters.

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
hideckies profile image
hideckies

Hi,

How do I make it so that it uses the second character only if a t-shirt is selected to be displayed?

Such a case, you can add the optional code after random.choice() in gen_metadata.py

def rand_attributes(id):
    # ...
    random_model = random.choice(...
    random_clothes = random.choice(...

    # Add it!
    if random_clothes == "t-shirts":
        random_model = "another"    # assumed that the model names are "original" and "another"

    # ...
    random_model = random_model.replace(...
    random_clothes = random_clothes.replace(...

    attributes = [...]
Enter fullscreen mode Exit fullscreen mode
Collapse
 
raul_proppe profile image
Proppe

Hi hideckies I'm using your code (thanks btw). I made some NFTS to test and I notice that the PNG number differs from the JSON file number after 3 renders. Is that a reason for that? Thank you for you time.

Collapse
 
shekhar04487389 profile image
chapapa

Thanks bruvโ€ฆgreat help. Iโ€™ve been looking for this since so long haha

Collapse
 
teampermanent profile image
Permanent ยฎ

Hey, amazing information here, thanks a lot! Is It possible to create that kind of script in cinema4d?

Collapse
 
hideckies profile image
hideckies

Thank you! But sorry, I don't know about Cinema4D.

Collapse
 
k0nd0r3 profile image
Serie Tv Streaming Sub ITA

hello, thank you it was really very useful
I have a question
I only have 1 model, 1 head
to this I want to apply different textures and backgrounds
in order to release various versions

is it possible to do this with this script?

Thread Thread
 
hideckies profile image
hideckies

In that case, add multiple materials(textures) to the object (e.g. material names are "material_1", "material_2", so on) in advance, then you can write for example:

# gen_metadata.py

# List
list_background = [black, red, green, ...]
list_head = [material_1, material_2, ...]

random_background = random.choice(list_background)
# ...

attributes = [
    {
        "trait_type": "Background",
        "value": random_background
    },
    # ...
]
Enter fullscreen mode Exit fullscreen mode
# gen_model.py

# Assign background material
def assign_background_material(material_name):
    # Get the object named "background" in the "misc" collection from the scene
    background_object = bpy.data.objects["background"]

    # Set the material you want to assign
    target_material = bpy.data.materials.get(material_name)

    # Assign material
    background_object.data.materials[0] = target_material

# Append head
def append_head(trait_value):
    # Set the material name (e.g. "material_1", "material_2", etc)
    material_name = trait_value

    # Append "head" collection in the scene
    path = PARTS_DIR + "head/head.blend/Collection/"
    bpy.ops.wm.append(filename="head", directory=path)

    # Get the "head" collection
    head_col = bpy.data.collections["head"]

     # Assign material
    target_material = bpy.data.materials.get(material_name)
    for head in head_col.objects:
        head.data.materials[0] = target_material

# Generate
def generate(id, metadata):
    # Background
   if attr in metadata["attributes"]:
        if attr["trait_type"] == "Background" and attr["value"] != "":
            assign_background_material(attr["value"])
    # ....
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kite16 profile image
kite16

how do I run this on a render farm, should I code it so that it makes 4444 .blend files?
I would appreciate it if you could include the code in the comments :))

Collapse
 
hideckies profile image
hideckies

Sorry I don't use a render farm, so I have no idea.

Collapse
 
katonacsaba profile image
CsabaKatona

Hey, is there any way that i could generate to .blender files instead of PNG?

Collapse
 
Sloan, the sloth mascot
Comment deleted
Collapse
 
hideckies profile image
hideckies

What's the IRL Apes SC? I'm not a blender master, but I'm interested.

Collapse
 
hideckies profile image
hideckies

Exactly, I've added the demo repository.
Please check it!

github.com/hideckies/nft-collectib...

We need you.

We're hiring for a Senior Full Stack Engineer and would love for you to apply. Head here to learn more about who we're looking for.