Skip to content

Instantly share code, notes, and snippets.

@blender8r
Created May 25, 2023 12:51
Show Gist options
  • Select an option

  • Save blender8r/3ea047af33381d0e41b8009115ccd0b8 to your computer and use it in GitHub Desktop.

Select an option

Save blender8r/3ea047af33381d0e41b8009115ccd0b8 to your computer and use it in GitHub Desktop.
Creates a Blender cel shader with the controls gathered into one group
import bpy
IMAGE = None
TEXTURE_PATH = 'D:\\images\\textures\\hatch_1.jpg'
for i in bpy.data.images:
if i.filepath == TEXTURE_PATH:
IMAGE = i
if not IMAGE:
IMAGE = bpy.data.images.load(TEXTURE_PATH)
def add_group_controls(group, input_type, input_name, def_val, min_val=0.0, max_val=1.0):
ctrl = group.inputs.new(input_type, input_name)
ctrl.default_value = def_val
if input_type == 'NodeSocketFloat' or input_type == 'NodeSocketFloatFactor':
ctrl.min_value = min_val
ctrl.max_value = max_val
# based on TLousky's function posted in this thread
# https://blender.stackexchange.com/questions/39127/how-to-put-together-a-driver-with-python/39129#39129
def add_driver(source, target, prop, dataPath, index = -1, negative = False, func = ''):
if index != -1:
d = source.driver_add(prop, index).driver
else:
d = source.driver_add(prop).driver
v = d.variables.new()
v.name = prop
v.targets[0].id_type = 'MATERIAL'
v.targets[0].id = target
v.targets[0].data_path = dataPath
d.expression = func + "(" + v.name + ")" if func else v.name
d.expression = d.expression if not negative else "-1 * " + d.expression
return d
# create an ico sphere and set to smooth shade
bpy.ops.mesh.primitive_ico_sphere_add()
sphere = bpy.context.active_object
for p in sphere.data.polygons:
p.use_smooth = True
# add subd modifier
subd = sphere.modifiers.new('subsurf', type='SUBSURF')
subd.levels = 3
# create new material
mat_name = 'Ghibli Hatch Shader'
mat = bpy.data.materials.new(mat_name)
mat.use_nodes = True
node_tree = mat.node_tree
#nodes = node_tree.nodes
links = node_tree.links
sphere.data.materials.append(mat)
# create control group
group = bpy.data.node_groups.new(type='ShaderNodeTree', name='Ghibli Shader')
nodes = group.nodes
# add inputs with calls to our add input function
add_group_controls(group, 'NodeSocketColor', 'Color A', (0.015, 0.125, 0.013, 1.0))
add_group_controls(group, 'NodeSocketColor', 'Color B', (0.481, 0.464, 0.153, 1.0))
add_group_controls(group, 'NodeSocketFloatFactor', 'Color A Clamp', 0.0)
add_group_controls(group, 'NodeSocketFloatFactor', 'Color B Clamp', 0.7)
add_group_controls(group, 'NodeSocketFloatFactor', 'Noise Strength', 0.75)
add_group_controls(group, 'NodeSocketFloat', 'Noise Scale', 5.0, 0.0, 200.0)
add_group_controls(group, 'NodeSocketFloatFactor', 'Noise Variation', 0.8)
add_group_controls(group, 'NodeSocketFloatFactor', 'Noise Min', 0.0)
add_group_controls(group, 'NodeSocketFloatFactor', 'Noise Max', 1.0)
add_group_controls(group, 'NodeSocketFloatFactor', 'Noise Soft Min', 0.0)
add_group_controls(group, 'NodeSocketFloatFactor', 'Color Soft Max', 0.9)
add_group_controls(group, 'NodeSocketFloatFactor', 'Speckle Strength', 0.8)
add_group_controls(group, 'NodeSocketFloat', 'Speckle Scale', 25.0, 0.0, 50.0)
add_group_controls(group, 'NodeSocketFloatFactor', 'Shadow Blend', 1.0)
add_group_controls(group, 'NodeSocketFloatFactor', 'Hatching Blend', 1.0)
add_group_controls(group, 'NodeSocketVector', 'Hatching Scale', (8.0, 8.0, 8.0))
add_group_controls(group, 'NodeSocketFloatFactor', 'Hatching Falloff Min', 0.136)
add_group_controls(group, 'NodeSocketFloatFactor', 'Hatching Falloff Max', 0.291)
# create group inputs and outputs
group.outputs.new('NodeSocketShader', 'Output1')
output_node = group.nodes.new('NodeGroupOutput')
input_node = group.nodes.new('NodeGroupInput')
# assign the group to the main node tree
group_node = node_tree.nodes.new('ShaderNodeGroup')
group_node.node_tree = group
ShaderNodeOutputMaterial_node = node_tree.nodes['Material Output']
links.new(group_node.outputs[0], ShaderNodeOutputMaterial_node.inputs[0])
# get main shader material output node
node_mat_output = node_tree.nodes['Material Output']
# delete default bsdf node
node_tree.nodes.remove(node_tree.nodes['Principled BSDF'])
# create new nodes and set their values
node_emission = nodes.new(type='ShaderNodeEmission')
node_mix1 = nodes.new(type='ShaderNodeMixRGB')
node_mix1.blend_type = 'MULTIPLY'
node_mix1.inputs['Fac'].default_value = 1.0
node_mix1.label = 'Ink Effect Blend'
node_mix2 = nodes.new(type='ShaderNodeMixRGB')
node_mix2.blend_type = 'SOFT_LIGHT'
node_mix2.inputs['Fac'].default_value = 0.75
node_mix2.label = 'Secondary Noise Strength'
node_mix3 = nodes.new(type='ShaderNodeMixRGB')
node_mix3.blend_type = 'SOFT_LIGHT'
node_mix3.label = 'Hatching Effect Blend'
node_mix4 = nodes.new(type='ShaderNodeMixRGB')
node_mix4.blend_type = 'MIX'
node_mix4.inputs['Fac'].default_value = 0.8
node_mix4.label = 'Speckle Effect Blend'
node_color_ramp1 = nodes.new(type='ShaderNodeValToRGB')
node_color_ramp1.color_ramp.elements[0].position = 0.0
node_color_ramp1.color_ramp.elements[0].color = (0.0373, 0.0752, 0.0305, 1.0)
node_color_ramp1.color_ramp.elements[1].position = 0.7
node_color_ramp1.color_ramp.elements[1].color = (0.4351, 0.4297, 0.0953, 1.0)
node_color_ramp1.label = 'Secondary Color Blend'
node_color_ramp2 = nodes.new(type='ShaderNodeValToRGB')
node_color_ramp2.color_ramp.elements[0].position = 0.15
node_color_ramp2.color_ramp.elements[1].position = 0.3
node_color_ramp2.label = 'Hatching Falloff'
node_color_ramp3 = nodes.new(type='ShaderNodeValToRGB')
node_color_ramp3.label = 'Voronoi Strength'
node_color_ramp4 = nodes.new(type='ShaderNodeValToRGB')
node_color_ramp4.color_ramp.elements[0].position = 0.5
node_color_ramp4.color_ramp.elements[1].position = 0.7
node_color_ramp4.label = 'Speckle Falloff'
node_color_ramp5 = nodes.new(type='ShaderNodeValToRGB')
node_color_ramp5.color_ramp.elements[0].position = 0.0
node_color_ramp5.color_ramp.elements[1].position = 0.9
node_color_ramp5.label = 'Voronoi Softness Variation'
node_img_tex = nodes.new(type='ShaderNodeTexImage')
node_img_tex.image = IMAGE
node_img_tex.label = 'Hatching Texture Map'
node_shader2RGB = nodes.new(type='ShaderNodeShaderToRGB')
node_mapping = nodes.new(type='ShaderNodeMapping')
node_mapping.inputs[3].default_value = (8.0, 8.0, 8.0)
node_tex_coords = nodes.new(type='ShaderNodeTexCoord')
node_diff_bsdf = nodes.new(type='ShaderNodeBsdfDiffuse')
node_voronoi = nodes.new(type='ShaderNodeTexVoronoi')
node_voronoi.feature = 'SMOOTH_F1'
node_voronoi.inputs[2].default_value = 10.0
node_musgrave = nodes.new(type='ShaderNodeTexMusgrave')
node_musgrave.inputs[2].default_value = 25.0
node_noise = nodes.new(type='ShaderNodeTexNoise')
node_noise.inputs[2].default_value = 5.0
node_noise.inputs[4].default_value = 0.5
# link nodes together
group.links.new(output_node.inputs['Output1'], node_emission.outputs['Emission'])
group.links.new(node_emission.inputs['Color'], node_mix1.outputs['Color'])
group.links.new(node_mix1.inputs['Color1'], node_color_ramp1.outputs['Color'])
group.links.new(node_mix1.inputs['Color2'], node_color_ramp2.outputs['Color'])
group.links.new(node_color_ramp1.inputs['Fac'], node_mix2.outputs['Color'])
group.links.new(node_color_ramp2.inputs['Fac'], node_mix3.outputs['Color'])
group.links.new(node_mix2.inputs['Color2'], node_mix4.outputs['Color'])
group.links.new(node_mix4.inputs['Color2'], node_color_ramp4.outputs['Color'])
group.links.new(node_color_ramp4.inputs['Fac'], node_musgrave.outputs['Fac'])
group.links.new(node_mix4.inputs['Color1'], node_color_ramp3.outputs['Color'])
group.links.new(node_color_ramp3.inputs['Fac'], node_voronoi.outputs['Color'])
group.links.new(node_voronoi.inputs['Smoothness'], node_color_ramp5.outputs['Color'])
group.links.new(node_color_ramp5.inputs['Fac'], node_noise.outputs['Color'])
group.links.new(node_mix2.inputs['Color1'], node_shader2RGB.outputs['Color'])
group.links.new(node_mix3.inputs['Color1'], node_shader2RGB.outputs['Color'])
group.links.new(node_shader2RGB.inputs['Shader'], node_diff_bsdf.outputs['BSDF'])
group.links.new(node_mix3.inputs['Color2'], node_img_tex.outputs['Color'])
group.links.new(node_img_tex.inputs['Vector'], node_mapping.outputs['Vector'])
group.links.new(node_mapping.inputs['Vector'], node_tex_coords.outputs['UV'])
# link group controls to shader node inputs
group.links.new(node_mix2.inputs['Fac'], input_node.outputs['Noise Strength'])
group.links.new(node_noise.inputs['Scale'], input_node.outputs['Noise Variation'])
group.links.new(node_voronoi.inputs['Scale'], input_node.outputs['Noise Scale'])
group.links.new(node_mix4.inputs['Fac'], input_node.outputs['Speckle Strength'])
group.links.new(node_musgrave.inputs['Scale'], input_node.outputs['Speckle Scale'])
group.links.new(node_mix1.inputs['Fac'], input_node.outputs['Shadow Blend'])
group.links.new(node_mix3.inputs['Fac'], input_node.outputs['Hatching Blend'])
group.links.new(node_mapping.inputs['Scale'], input_node.outputs['Hatching Scale'])
# add drivers for color ramps
driver = add_driver(node_color_ramp1.color_ramp.elements[0], mat, 'position', 'node_tree.nodes["Group"].inputs["Color A Clamp"].default_value')
driver = add_driver(node_color_ramp1.color_ramp.elements[1], mat, 'position', 'node_tree.nodes["Group"].inputs["Color B Clamp"].default_value')
driver = add_driver(node_color_ramp3.color_ramp.elements[0], mat, 'position', 'node_tree.nodes["Group"].inputs["Noise Min"].default_value')
driver = add_driver(node_color_ramp3.color_ramp.elements[1], mat, 'position', 'node_tree.nodes["Group"].inputs["Noise Max"].default_value')
driver = add_driver(node_color_ramp5.color_ramp.elements[0], mat, 'position', 'node_tree.nodes["Group"].inputs["Noise Soft Min"].default_value')
driver = add_driver(node_color_ramp5.color_ramp.elements[1], mat, 'position', 'node_tree.nodes["Group"].inputs["Noise Soft Max"].default_value')
driver = add_driver(node_color_ramp2.color_ramp.elements[0], mat, 'position', 'node_tree.nodes["Group"].inputs["Hatching Falloff Min"].default_value')
driver = add_driver(node_color_ramp2.color_ramp.elements[1], mat, 'position', 'node_tree.nodes["Group"].inputs["Hatching Falloff Max"].default_value')
# add drivers for color ramp 1 colors per channel
driver = add_driver(node_color_ramp1.color_ramp.elements[0], mat, 'color', 'node_tree.nodes["Group"].inputs["Color A"].default_value[0]', 0)
driver = add_driver(node_color_ramp1.color_ramp.elements[0], mat, 'color', 'node_tree.nodes["Group"].inputs["Color A"].default_value[1]', 1)
driver = add_driver(node_color_ramp1.color_ramp.elements[0], mat, 'color', 'node_tree.nodes["Group"].inputs["Color A"].default_value[2]', 2)
driver = add_driver(node_color_ramp1.color_ramp.elements[0], mat, 'color', 'node_tree.nodes["Group"].inputs["Color A"].default_value[3]', 3)
driver = add_driver(node_color_ramp1.color_ramp.elements[1], mat, 'color', 'node_tree.nodes["Group"].inputs["Color B"].default_value[0]', 0)
driver = add_driver(node_color_ramp1.color_ramp.elements[1], mat, 'color', 'node_tree.nodes["Group"].inputs["Color B"].default_value[1]', 1)
driver = add_driver(node_color_ramp1.color_ramp.elements[1], mat, 'color', 'node_tree.nodes["Group"].inputs["Color B"].default_value[2]', 2)
driver = add_driver(node_color_ramp1.color_ramp.elements[1], mat, 'color', 'node_tree.nodes["Group"].inputs["Color B"].default_value[3]', 3)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment