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.
-
$\begingroup$ 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 $\endgroup$– Duarte Farrajota Ramos ♦Commented Jan 23, 2017 at 4:48
-
$\begingroup$ The 'seams' are a result of using UV mapping at the transition between faces - try using Object coordinates instead. $\endgroup$– Rich SedmanCommented Jan 23, 2017 at 10:07
1 Answer
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 (Blender versions prior to 2.80 - for later versions, see EDIT further down post) :
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 = {}
vert_links = {}
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.
EDIT for Blender 2.80+ (changes to API): At later Blender versions the API function calls for some operations has changed. Here's an updated script that works at later releases (tested on 2.91).
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')
#For 2.80+, changed API for selecting object
#obj.select = True
obj.select_set(state = True)
#For 2.80+, changed API to set active object
#bpy.context.scene.objects.active = obj
bpy.context.view_layer.objects.active = obj
bpy.ops.object.mode_set(mode='EDIT')
obj.update_from_editmode()
mesh = obj.data
scn = bpy.context.scene
#2.80+ this isn't necessary (already made it 'active')
#scn.objects.active = obj
start_vertices = [] # Start RED
end_vertices = [] # End RED
seam_vertices = [] # GREEN
crease_vertices = [] # Block influence
vert_colors = {}
vert_links = {}
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()
#Removed for 2.80+ since no longer valid and not needed
#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
#In 2.80+, use the '@' operator for matrix mulitplication
#v1Pos = OWMatrix * obj.data.vertices[vert1].co
#v2Pos = OWMatrix * obj.data.vertices[vert2].co
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["Suzanne"], 1250, 0.0001,'START', 'END', 'SEAM', 'CREASE')