# How Can I Use a Procedural Bump Map to Replicate a Natural Pattern?

So I have tried for up to five hours to try to use the nodes to make it so that the 'veins' on the image are shown on the model with a bump map. Any insights on having the 'veins' run parallel to the shape of the antlers? Bonus would be to remove the seams between the different regions of the geometry.

• That might be just a little too much to ask for procedural textures alone. Either you can achieve that through a good UV Map for your mesh, or you might just have to go with hand made image textures – Duarte Farrajota Ramos Jan 23 '17 at 4:48
• The 'seams' are a result of using UV mapping at the transition between faces - try using Object coordinates instead. – Rich Sedman Jan 23 '17 at 10:07

To align the texture along the direction of the mesh requires a UV map and this needs to be unwrapped in such a way that each face is oriented along the line of the antler. Further, to avoid a 'seam' along the length of the mesh requires that a seamless texture is used and that the UV map unwrapping ensures that the edges of the UV map align. The right UV map and a seamless texture can produce the following :

Manually unwrapping such a mesh while keeping the faces correctly aligned and joined at the seams would be quite complicated and time consuming - unwrapping the splits while keeping the orientation and the seams would be particularly difficult. Fortunately, Python can do the unwrapping for us.

Starting with a mesh created using the Skin modifier :

The 'texture' needs to run from one end of the antler (the 'root) to the tips. Create two new vertex groups (one for the 'start' and the other for the 'end') and assign the relevant vertices to each. For the example above I marked the vertices around the 'root' of the antler as 'START' and those at the tips as 'END'. The texture will be spread over the mesh using those points as anchors.

Next, create another vertex group ('seam') and assign vertices along the length of each 'branch' to it. This will be the 'seam' of the texture as it's wrapped around the mesh. You can experiment with different seams to get twists and different patterns for the split but the pattern shown below works quite well.

To give further control at the boundaries of branches you can allocate vertices to a 'crease' vertex group. This will effectively block the influence of the 'seam' to prevent one branch's seam from affecting the next. You may need some experimentation for this - depending on your geometry and chosen seam. In my case I simply 'capped' the end of each seam.

Open a Text Editor window and paste the following code :

import bpy
import bmesh

group_vertex_cache = []

def color_vertices_rgb(obj, numpasses, target_delta, grp_start, grp_end, grp_seam, grp_crease):
"""Uses the Vertex Groups to assign each vertex UV coordinates, blended across the mesh"""

bpy.ops.object.mode_set(mode='OBJECT')
bpy.ops.object.select_all(action='DESELECT')
obj.select = True
bpy.context.scene.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')

obj.update_from_editmode()

mesh = obj.data
scn = bpy.context.scene

scn.objects.active = obj

start_vertices = []     # Start RED
end_vertices = []       # End RED
seam_vertices = []      # GREEN
crease_vertices = []    # Block influence
vert_colors = {}

global group_vertex_cache
init_group_vertex_cache(obj)

# Get a count of loop verts and create an array to hold the details
loopvert_count = len(obj.data.loops)
loopverts = [0] * loopvert_count

print('Capturing...')
# Process each loop vert and collect all the data we require
for polygon in obj.data.polygons:

loopidxpos = -1
for loopidx in polygon.loop_indices:
loopidxpos = loopidxpos + 1

loopvertrec = {}

# Determine which loop verts are to the left and right of this one
if loopidxpos == 0:
left  = polygon.loop_indices[len(polygon.loop_indices) - 1]
right = polygon.loop_indices[loopidxpos + 1]
elif loopidxpos == (len(polygon.loop_indices)-1):
left  = polygon.loop_indices[loopidxpos - 1]
right = polygon.loop_indices[0]
else:
left  = polygon.loop_indices[loopidxpos - 1]
right = polygon.loop_indices[loopidxpos + 1]

loopvertrec['left']  = left
loopvertrec['right'] = right
loopvertrec['left-dist'] = get_edge_length(obj, loopidx, left)
loopvertrec['right-dist'] = get_edge_length(obj, loopidx, right)

# get list of loops verts that make up the same vert as this one
loopvertrec['shared-loop-verts'] = get_loop_verts(obj, loopidx)

#...get whether it's in each of the 'groups' (ie, start, end, seam, crease) - and the associated weight (assume weight 0 if not in group)
loopvertrec['start-weight'] = get_group_weight(obj, grp_start, loopidx)
loopvertrec['end-weight'] = get_group_weight(obj, grp_end, loopidx)
loopvertrec['seam-weight'] = get_group_weight(obj, grp_seam, loopidx)
loopvertrec['crease-weight'] = get_group_weight(obj, grp_crease, loopidx)

#loopvertrec['color'] = get_loopvert_color(obj, loopidx)

loopverts[loopidx] = loopvertrec
print(str(loopidx))

vert_colors[loopidx] = [0.5,0.5,0.5]

print('Captured.')

new_colors = {}
print('Processing....')
for p in range(0, numpasses):
print("Pass "+str(p+1)+" of "+str(numpasses))

maxdelta = 0

for v in range(0, len(loopverts)):

loopvertrec = loopverts[v]

start_weight  = loopvertrec['start-weight']
end_weight    = loopvertrec['end-weight']
seam_weight   = loopvertrec['seam-weight']
crease_weight = loopvertrec['crease-weight']

# Generate Red, Green, Blue based on weights and spread over mesh. Red will be used to generate U, Green/Blue to generate V

# Average colors with neighbours
r = (vert_colors[v][0] + vert_colors[loopvertrec['left']][0] + vert_colors[loopvertrec['right']][0]) / 3
g = (vert_colors[v][1] + vert_colors[loopvertrec['left']][1] + vert_colors[loopvertrec['right']][1]) / 3
b = (vert_colors[v][2] + vert_colors[loopvertrec['left']][2] + vert_colors[loopvertrec['right']][2]) / 3

average_this_loop = [r, g, b]

for v2 in loopvertrec['shared-loop-verts']:
r = r + (vert_colors[v][0] + vert_colors[loopverts[v2]['left']][0] + vert_colors[loopverts[v2]['right']][0]) / 3
g = g + (vert_colors[v][1] + vert_colors[loopverts[v2]['left']][1] + vert_colors[loopverts[v2]['right']][1]) / 3
b = b + (vert_colors[v][2] + vert_colors[loopverts[v2]['left']][2] + vert_colors[loopverts[v2]['right']][2]) / 3

average_all_loops = [r / (len(loopvertrec['shared-loop-verts'])+1),g / (len(loopvertrec['shared-loop-verts'])+1),b / (len(loopvertrec['shared-loop-verts'])+1)]

# CREASE
r = average_all_loops[0]
g = average_this_loop[1] * ( crease_weight ) + average_all_loops[1] * ( 1.0 - crease_weight )
b = average_this_loop[2] * ( crease_weight ) + average_all_loops[2] * ( 1.0 - crease_weight )

r = r * (1.0 - end_weight)                          # END
r = r * (1.0 - start_weight) + 1.0 * start_weight   # START

# Determine the left/right side of the seam based on direction of 'red'
if (vert_colors[loopvertrec['left']][0] > vert_colors[loopvertrec['right']][0]):
leftness = 1.0
elif (vert_colors[loopvertrec['left']][0] < vert_colors[loopvertrec['right']][0]):
leftness = 0.0
else:
leftness = 0.5

g = g * (1.0 - seam_weight) + leftness * seam_weight

# store it
if len(new_colors)>v:
vertdelta = abs(new_colors[v][0]-r) + abs(new_colors[v][1]-g) + abs(new_colors[v][2]-b)
else:
vertdelta = abs(r) + abs(g) + abs(b)

if vertdelta > maxdelta:
maxdelta = vertdelta

new_colors[v] = (r,g,b)

print(maxdelta)
if maxdelta < target_delta:
break

#Copy new colors back to vert_colors
for i in range(0,len(loopverts)):
vert_colors[i] = new_colors[i]

# Generate UV map - must be in EDIT mode for this

bm = bmesh.from_edit_mesh(obj.data)
uv_layer = bm.loops.layers.uv.verify()
bm.faces.layers.tex.verify()

for f in bm.faces:
for l in f.loops:
luv = l[uv_layer]

r = vert_colors[l.index][0]
g = vert_colors[l.index][1]
b = vert_colors[l.index][2]

luv.uv = (r, g)

bmesh.update_edit_mesh(obj.data)
bpy.ops.object.mode_set(mode='OBJECT')

###########################################################################################################################

def get_edge_length(obj, loopidx1, loopidx2):

vert1 = obj.data.loops[loopidx1].vertex_index
vert2 = obj.data.loops[loopidx2].vertex_index

for edge in obj.data.edges:
if (edge.vertices[0] == vert1 and edge.vertices[1] == vert2) or (edge.vertices[0] == vert2 and edge.vertices[1] == vert1):

OWMatrix = obj.matrix_world
v1Pos = OWMatrix * obj.data.vertices[vert1].co
v2Pos = OWMatrix * obj.data.vertices[vert2].co

dist = (v1Pos - v2Pos).length

return dist

return 0.0

###########################################################################################################################

def get_loop_verts(obj, loopidx):
loopverts = []
vertidx = obj.data.loops[loopidx].vertex_index

idx = -1
for loop in obj.data.loops:
idx = idx + 1
if loop.vertex_index == vertidx and idx != loopidx:
loopverts.append(idx)

return loopverts

###########################################################################################################################

def get_group_weight(obj, group, loopidx):

groupindex = obj.vertex_groups[group].index
vertidx = obj.data.loops[loopidx].vertex_index

global group_vertex_cache

for v in group_vertex_cache:
if v.index == vertidx:
for g in v.groups:
if g.group == groupindex:
return g.weight

return 0.0

###########################################################################################################################

def init_group_vertex_cache(obj):

global group_vertex_cache

for v in obj.data.vertices:
if len(v.groups) > 0:
group_vertex_cache.append(v)

###########################################################################################################################

#Invoke it
color_vertices_rgb(bpy.context.scene.objects["Antler"], 1250, 0.0001,'START', 'END', 'SEAM', 'CREASE')


Note the last line of the script which invokes the function on your mesh. This should be amended to specify the name of your mesh and the other arguments as follows :

color_vertices_rgb(bpy.context.scene.objects["<meshname>"], <iterations>, <mindelta>, '<start>', '<end>', '<seam>', '<crease>')


Where meshname is the name of your mesh, iterations is the number of steps to run the 'average' spread algorithm, mindelta is the minimum acceptable change in UV coordinates (if it reaches that then it will decide it's finished regardless of iterations - if unsure, set to 0), start, end, seam, crease are the names of the 4 vertex groups mentioned above. Run the script - it might take a while depending on the complexity of your mesh and the chosen number of iterations (more will produce better results but will take longer). If you have a command-line console window open it will output progress and diagnostics.

This works by allocating UV coordinates (which can be represented as 'colors' - Red being equivalent to 'U' and Green being equivalent to 'V') to each of the vertices in the groups and spreading them across the mesh by averaging them with their immediate neighbours.

Note that the Red is strongest at the root and fades out towards the tips while the green is strongest at the left-hand edge of the seam and fades around the circumference of the mesh.

Once the processing is complete you should have a UV Map that you can use for your texture and this can be used with a seamless image texture if desired - although depending on the texture used, this isn't strictly necessary if the texture is already quite linear since it effectively hides the seam. For example, here's a simple Musgrave texture, scaled in one direction using a Mapping node.

Combine multiple textures at different scales for a more interesting surface.

This can now be used to drive the Bump node and adjusted to get the desired effect.

Blend file attached