First of all I would like you to picture yourself taking a 2D bit of gift wrapping, and wrapping a 3D gift you are about to give to someone. This in a nutshell is UV unwrapping / wrapping and this visual might help you.

We’re going to start with the basic python script we covered in part 1.
We used this script to design and build a simple triangle face by defining 3D points in a data structure, transfering the data to a mesh, and finally adding a UV layer to enable textures & material addition.

# Import Blender API modules
import bpy
import bmesh

# Create a new mesh
mesh = bpy.data.meshes.new("MyMesh")

# Create a new object and assign it to the mesh.
obj_name="MyObject"
obj = bpy.data.objects.new(obj_name, mesh)

# Get a handle to the current scene in Blender.
scene = bpy.context.scene

# Link our object to the scene.
scene.collection.objects.link(obj)

# Create a BMesh data structure object.
bm = bmesh.new()

# Define the vertices coordinates and add them to the BMesh data structure.
v1 = bm.verts.new((0, 0, 0))
v2 = bm.verts.new((1, 0, 0))
v3 = bm.verts.new((0, 1, 0))

# Define a face by stating the bounding vertices and add to the BMesh data structure.
bm.faces.new((v1, v2, v3))

# Transfer the data in the BMesh data structure to the mesh.
bm.to_mesh(mesh)
# Clear and release the memory used by the BMesh data object.
bm.free()


# Create a new UV layer if it doesn't exist
if not mesh.uv_layers:
    mesh.uv_layers.new()

Reset Scale & Rotation

Scale and rotation issues can really really mess up the application of textures and images to your objects.
So, an easy fix is just to reset them.

Rotation

Set the rotation of the current selected object to be equivalent to zero in each plane. The current object rotation will be considered to be the “default rotations”.

Scale

Set the scale of the current selected object to be equivalent to 0 in each plane. The current object scale will be considered to be the “default scale”

Code

Here is how to do this in code. ( CTRL+A Select rotation, and scale, if you are doing this in the GUI)

# RESET SCALE AND ROTATION

# Reset rotation
# Set rotation to (0, 0, 0) in Euler angles
obj.rotation_euler = (0, 0, 0)

# Reset scale
# Set scale to (1, 1, 1) uniformly
obj.scale = (1, 1, 1)

In the blender GUI, this is what a reset scale and rotation looks like,

Create UV Grid Image

A UV grid image, also known as a UV layout or UV template, is an image that displays a grid pattern with coordinates to help visualize the UV mapping of a 3D mesh. It helps you adjust the orientation of any image you want to apply to your 3D object.

# CREATE UV-GRID IMAGE
image_name='MyUV_Grid'

# Check if an image with the same name already exists
if image_name in bpy.data.images:
    image = bpy.data.images[image_name]
else:
    # Create a new image
    image = bpy.data.images.new(
        name=image_name,
        width=1024,
        height=1024,
        alpha=False,
        float_buffer=False,
        stereo3d=False,
        is_data=False,
        tiled=False
    )

    # Specify source and type
    image.source = 'GENERATED'
    image.generated_type = 'UV_GRID'

# Update the image to reflect the changes
image.update()

You can already preview the generated UV grid image in UV Editing mode, here is what you would see,

you can imagine having a nice fitting image would have clean and neat right angles.

Create New Material & Apply To Object

This code in this part looks a little complicated.
The visual effects of an object in blender are a just a chain of specific operations, or properties.
Each specific operation, or property, is called a node.
So to apply images, materials to an object you need to create a chain of nodes starting with the default “Principled BSDF” node.
You can see these nodes in the “Shading” editor view.

Keep in mind the 3 nodes you see above,

  • Principled BSDF: The root node.
  • MyUV_Grid: The image texture node holding the image we created.
  • Material Output: The output node. We set the output to the surface mode

This should make the following code more understandable. I’ve added comments.
In short, this code creates the nodes, then links the nodes together.

#CREATE A NEW MATERIAL & APPLY TO OBJECT

#Create a new material
material_name = "MyNewMaterial"

# Check if the material already exists
material = bpy.data.materials.get(material_name)

if material is None:
    material = bpy.data.materials.new(name=material_name)

# In the context of the Blender Node Editor, a "node" 
# refers to the individual elements on an input/output flow graph 
# that perform specific operations or functions. 
# These nodes are visual representations of computational units 
# that can be connected together to create a larger network that 
# defines the shading and appearance of materials. Each node represents 
# a specific operation or property, such as texture generation, color mixing, 
# or material properties. They have input sockets that receive data and output
# sockets that send data to other nodes. By linking the output of one node to 
# the input of another, you can control the flow of data and create complex material behaviors.
# Yea, so not a nodes on a 3D object. 

#Enable the use of nodes.
material.use_nodes = True

#Get the node tree of the material
tree = material.node_tree

#Clear the existing nodes from the node tree.
#For a brand new material these will be cleared anyway, but
#good practice if you are making changes.
nodes = tree.nodes
nodes.clear()

# Create the root/principle BSDF node.
node_principled = nodes.new(type='ShaderNodeBsdfPrincipled')
# BSDF node is a versatile and common node used in blender.
# BSDF (Bidirectional Scattering Distribution Function) node 
# represents a shader node that defines the shading properties 
# of a material's surface.

# Create the image texture node
node_texture = nodes.new(type='ShaderNodeTexImage')

#Assign the material to the object
node_texture.image = image

# Connect the image texture node to the principled BSDF node
links = tree.links
links.new(node_texture.outputs['Color'], node_principled.inputs['Base Color'])

# Create the material output node
node_output = nodes.new(type='ShaderNodeOutputMaterial')

# Link the principled BSDF node to the material output node
links.new(node_principled.outputs['BSDF'], node_output.inputs['Surface'])

# Assign the material to the object
obj.data.materials.append(material)

The application of an image material is never smooth. Take a look at what you get, the squares are skew whiff. To fix this, a process called UV unwrapping is used. For simple shapes we can do this with code.

UV Unwrapping

The initial state before we unwrap our object is as follows.
On the left is our UV mapping image, and on the right a messy UV wrap.

The following python code unwraps the 3D object on the right, and places the face outlines on the UV image on the left. An “angle based” algorithm is used which unwraps the 3D object based on the angles between adjacent faces. The code comments should make some sense.

# UNWRAP THE OBJECT

# Select the object, and set to active. (Orange highlight)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)

# Switch to Edit Mode
bpy.ops.object.mode_set(mode='EDIT')

# Select all vertices
bpy.ops.mesh.select_all(action='SELECT')

# Unwrap the UVs  3D mesh, to 2D plane.
# Calculates and assigns UV coordinates to the vertices of the 
# selected mesh based on the specified unwrapping method 
# (e.g., angle-based, conformal, etc.) and any additional parameters 
# such as margin or padding.
bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.001)

#The method='ANGLE_BASED' argument specifies the unwrapping algorithm 
#to be used. In this case, the "ANGLE_BASED" method is chosen, which 
#unwraps the UVs based on the angles between adjacent faces. This method 
#aims to minimize stretching and distortion of the UVs.

#The margin=0.001 argument is optional and sets the margin or padding 
#between the UV islands. The value 0.001 represents a small value that 
#adds a tiny gap between the UV islands to avoid overlapping UV coordinates. 
#This small margin helps prevent visual artifacts or bleeding between the UV 
#islands.

# Switch back to Object Mode
bpy.ops.object.mode_set(mode='OBJECT')

This automatic angle based unwrapping algorithm does quite a good job. Here you can see the triangular face superimposed on the map on the left. The squares are projected properly on the object face on the right.

If you want to design textures to wrap around your 3D object, you can click UV -> Export UV Layout.
You can now edit this image file and re-import it to apply to your object.

Full Script

Here is the complete python script. Although for more complicated shapes using the GUI interface is much easier, I think implementing this in code for simple shapes first helps you grasp some fundamentals.

# Import Blender API modules
import bpy
import bmesh

# Create a new mesh
mesh = bpy.data.meshes.new("MyMesh")

# Create a new object and assign it to the mesh.
obj_name="MyObject"
obj = bpy.data.objects.new(obj_name, mesh)

# Get a handle to the current scene in Blender.
scene = bpy.context.scene

# Link our object to the scene.
scene.collection.objects.link(obj)

# Create a BMesh data structure object.
bm = bmesh.new()

# Define the vertices coordinates and add them to the BMesh data structure.
v1 = bm.verts.new((0, 0, 0))
v2 = bm.verts.new((1, 0, 0))
v3 = bm.verts.new((0, 1, 0))

# Define a face by stating the bounding vertices and add to the BMesh data structure.
bm.faces.new((v1, v2, v3))

# Transfer the data in the BMesh data structure to the mesh.
bm.to_mesh(mesh)
# Clear and release the memory used by the BMesh data object.
bm.free()


# Create a new UV layer if it doesn't exist
if not mesh.uv_layers:
    mesh.uv_layers.new()

# RESET SCALE AND ROTATION

# Reset rotation
# Set rotation to (0, 0, 0) in Euler angles
obj.rotation_euler = (0, 0, 0)

# Reset scale
# Set scale to (1, 1, 1) uniformly
obj.scale = (1, 1, 1)


# CREATE UV-GRID IMAGE
image_name='MyUV_Grid'

# Check if an image with the same name already exists
if image_name in bpy.data.images:
    image = bpy.data.images[image_name]
else:
    # Create a new image
    image = bpy.data.images.new(
        name=image_name,
        width=1024,
        height=1024,
        alpha=False,
        float_buffer=False,
        stereo3d=False,
        is_data=False,
        tiled=False
    )

    # Specify source and type
    image.source = 'GENERATED'
    image.generated_type = 'UV_GRID'

# Update the image to reflect the changes
image.update()


#CREATE A NEW MATERIAL & APPLY TO OBJECT

#Create a new material
material_name = "MyNewMaterial"

# Check if the material already exists
material = bpy.data.materials.get(material_name)

if material is None:
    material = bpy.data.materials.new(name=material_name)

# In the context of the Blender Node Editor, a "node" 
# refers to the individual elements on am input/output flow graph 
# that perform specific operations or functions. 
# These nodes are visual representations of computational units 
# that can be connected together to create a larger network that 
# defines the shading and appearance of materials. Each node represents 
# a specific operation or property, such as texture generation, color mixing, 
# or material properties. They have input sockets that receive data and output
# sockets that send data to other nodes. By linking the output of one node to 
# the input of another, you can control the flow of data and create complex material behaviors.
# Yea, so not a nodes on a 3D object. 

#Enable the use of nodes.
material.use_nodes = True

#Get the node tree of the material
tree = material.node_tree

#Clear the existing nodes from the node tree.
#For a brand new material these will be cleared anyway, but
#good practice if you are making changes.
nodes = tree.nodes
nodes.clear()

# Create the root/principle BSDF node.
node_principled = nodes.new(type='ShaderNodeBsdfPrincipled')
# BSDF node is a versatile and common node used in blender.
# BSDF (Bidirectional Scattering Distribution Function) node 
# represents a shader node that defines the shading properties 
# of a material's surface.

# Create the image texture node
node_texture = nodes.new(type='ShaderNodeTexImage')

#Assign the material to the object
node_texture.image = image

# Connect the image texture node to the principled BSDF node
links = tree.links
links.new(node_texture.outputs['Color'], node_principled.inputs['Base Color'])

# Create the material output node
node_output = nodes.new(type='ShaderNodeOutputMaterial')

# Link the principled BSDF node to the material output node
links.new(node_principled.outputs['BSDF'], node_output.inputs['Surface'])

# Assign the material to the object
obj.data.materials.append(material)

# UNWRAP THE OBJECT

# Select the object, and set to active. (Orange highlight)
bpy.context.view_layer.objects.active = obj
obj.select_set(True)

# Switch to Edit Mode
bpy.ops.object.mode_set(mode='EDIT')

# Select all vertices
bpy.ops.mesh.select_all(action='SELECT')

# Unwrap the UVs  3D mesh, to 2D plane.
# Calculates and assigns UV coordinates to the vertices of the 
# selected mesh based on the specified unwrapping method 
# (e.g., angle-based, conformal, etc.) and any additional parameters 
# such as margin or padding.
bpy.ops.uv.unwrap(method='ANGLE_BASED', margin=0.001)

#The method='ANGLE_BASED' argument specifies the unwrapping algorithm 
#to be used. In this case, the "ANGLE_BASED" method is chosen, which 
#unwraps the UVs based on the angles between adjacent faces. This method 
#aims to minimize stretching and distortion of the UVs.

#The margin=0.001 argument is optional and sets the margin or padding 
#between the UV islands. The value 0.001 represents a small value that 
#adds a tiny gap between the UV islands to avoid overlapping UV coordinates. 
#This small margin helps prevent visual artifacts or bleeding between the UV 
#islands.

# Switch back to Object Mode
bpy.ops.object.mode_set(mode='OBJECT')

Example

There are some small manual steps to take in the Blender GUI but mostly you are limited by your artistic talent. There are many good tutorials out there to give you the basics of sculpting, and further UV unwrapping hints and tips.

Here is a 3D barrel shape, with a 2D image which is mapped on to its surface.

As a games asset, your creations could look quite stunning.

Leave a Reply

%d bloggers like this: