5
$\begingroup$

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.

enter image description here

$\endgroup$
2
  • $\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$ 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$ Commented Jan 23, 2017 at 10:07

1 Answer 1

13
$\begingroup$

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 :

final result

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 :

mesh

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.

start end

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.

seam

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.

crease

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.

animated colors

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.

musgrave material musgrave

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

musgrave material2 musgrave2

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

bump

Blend file attached


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')
$\endgroup$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .