Cycles Brick Tricks 0.1

I love this, especially the fact that it is setup as an addon. Meaning I can drag projects to and from work without linked libraries not getting found and messing with missing files. Some of my thoughts about this:

  1. Switch to using the other (fac) slider style rather than the numeric input one. The inputs here are typically in the 0-1 range and the numeric control is just far too sensitive not allowing finetuning while dragging.

  2. I believe using distortion for noise outputs are better than using detail, and noise fac output should be used instead of color output if itā€™s only driving a single value. I believe there is an optimization coming out regarding monochrome noise outputs (only calculating one channel). I typically use 42.5, 0, and 42.5 for my noise outputs to produce color variations (cell id etc), using detail (octaves) for this is a waste of resources, whereas I believe distortion is free or cheap in comparison.

  3. Floorboard/plank generator would be a nice addition to this since itā€™s in the same realm of patterns, with individual control over width spacing (large, smooth) and longitudinal spacing (small, sharper) wrt feather. It would be very similar to the brick pattern, maybe with some control for offset randomness.

  4. Square/rounded square tiles (with or without sliding offset) is possible to have random rotation applied to. This require a fixed grout spacing (in addition to the feather) in order to have space for the rotation. It also require a random/noise/seed system within the group itself. I can post my own node group that does this if youā€™re interested, maybe you or Secrop can hack it to better suit the desired style.

  5. Possibly expand with other tilings? Google ā€œtiling patternsā€ for inspiration, and headaches :smiley:

  6. Maybe expand with other patterns such as wood or marble? Iā€™ve been thinking about this myself using UV to determine orientation and object coords to set an equal scale, but itā€™s outside my reach. Although Iā€™ve done some 3D patterning in the past, they quickly become too complex to keep track of, so Iā€™ve kind of avoided this for now.

Iā€™m finishing a converter script that turns nodegroups into python scriptsā€¦ Iā€™ll try post something this weekend.

Making the changes into the UI can be doneā€¦ and for making other patterns, will just be matter of building them with nodes, and convert them (LazyDodoā€™s scripts works with OSL, but I personally think the output script could have more optimizations).

Right now Iā€™m still working in making the scripted nodes interface as seamless as possible with the current node editor, but things are looking promissing. :slight_smile:

Howā€™s that thing coming along?

Anyhow, playing around with this thing, Iā€™m having issues that looks to be accuracy or modulo related on all of the uv_map_diamond outputs. Hooking it up to a noise 42.5, 0, 42.5 (vector input is object coords from a ā€œground planeā€), I get variations across square ā€œpixelsā€ rather than smooth when zooming in. I only have the addon version so I canā€™t investigate. It shows up clearly on the noise node, but also if using the outputs to drive a sharp glossy nodes bump, as the bump node will use a series of sharp transients at each pixel rather than a smooth transient. Same effect youā€™d get on a bumpmap with not enough resolution.

Hard to say whatā€™s going on without seeing a nodegraph and/or screenshots of the issue, but if you want to see whatā€™s going on inside the node here is the osl script for that node

shader uv_map_diamond(
    point Vector = P,
    float scale = 1.0, 
    float diamond_size = 0.2,
    float brick_width = 1.0,
    float brick_height = 1.0,
    float feather = 0.1,
    float border = 0.05,
    output point UV =0,
    output point CellID =0,
    output float Height = 0.0,
    output float TileMask = 0.0,
    output float BorderMask = 0.0,
    output float BrickWidth = 1.0,
    output float BrickHeight = 1.0,
    )
{
    float RealDiamond = min(diamond_size, min(brick_width, brick_height)/2);
    float hb = border/2;
    float hf = feather/2;
    BrickWidth = brick_width;
    BrickHeight = brick_height;
    float CellX = (int) (10000+(Vector[0]*scale)/brick_width);
    float CellY = (int) (10000+(Vector[1]*scale)/brick_height); 
    float rx = abs(fmod(10000+(Vector[0]*scale),brick_width));
    float ry = abs(fmod(10000+(Vector[1]*scale),brick_height));
    float x = min(rx, brick_width-rx);
    float y = min(ry, brick_height-ry);
    if (x + y < RealDiamond) 
    {
        float offsx=0;
        float offsy=0;
        BrickWidth = BrickHeight = RealDiamond*2;
        float addX =  0;
        if ((rx >  RealDiamond) && (ry < RealDiamond))
        {
            offsx=0.5;
            offsy=0.5;
            UV = point( RealDiamond-x-y,RealDiamond-(x-y),0); 
        }
        if ((rx >  RealDiamond) && (ry > RealDiamond))
        {
            offsx=0.5;
            offsy=1.5;
            UV = point( RealDiamond-(x-y) , RealDiamond-x-y,0); 
        }
        if ((rx <  RealDiamond) && (ry > RealDiamond))
        {   
            offsx=-0.5;
            offsy=1.5;
            UV = point( RealDiamond+ y+x,RealDiamond-(y-x) ,0); 
        }
        if ((rx < RealDiamond) && (ry < RealDiamond))
        {
            offsx=-0.5;
            offsy=0.5;
            UV = point( x+RealDiamond-y, RealDiamond+ y+x ,0); 
        }
        CellID = point(CellX+offsx, CellY+offsy,0);
        Height = min(RealDiamond-(x-y),RealDiamond-(y+x));
        TileMask = 1;
        BorderMask = Height < hb;
        Height = linearstep(hb,hb+(hf*(BrickWidth-hb)),Height); 
        
    }
    else
    {
       CellID = point(CellX, CellY,0);
       UV = point(rx,ry,0);
       if (ry < RealDiamond)
       {
           Height = min(x-(RealDiamond-y),y);
       }
       else if (ry > brick_height-RealDiamond)
       {
           Height = min(x-(RealDiamond-y),y);
       }
       else
           Height = min(x,y);
      BorderMask = Height < hb;
      Height = linearstep(hb,hb+(hf*(min(brick_width,brick_height)-hb)),Height);         
    }
    
}

Oh, I thought they were regular nodes, not OSL. Here is a couple of screenshots; the top two nodes gives a smooth UV readout, whereas the two bottom ones gives a pixelated UV readout making it unsuitable to derive bumps from. The actual nodes I would use would be more advanced, but this shows the jitter of it :slight_smile:



Very nice addon, @LazyDodo. Is there a built-in way to control grout/mortar colour or a way to grunge up the grout/mortar? Or failing that, any tips for creating grungy grout?

They are a little bit of both, I write the shaders in osl, then run them though some code that outputs a nodegroup you can run on the gpu. so you get the easy prototyping, while still being able to run them gpu accelerated once youā€™re done :slight_smile:

1 Like

Most nodes output a mask, use a noise texture for the grout then mix it in with the mask?

1 Like

That is weird. when i run it though the bump node, youā€™re right it gets all pixelated, but when you stick it straight into the displacement node, itā€™s smooth as butterā€¦ iā€™d have to see if i can make a smaller repro case and have the cycles guys take a peek at this.

Iā€™m curious why are you plugging uvā€™s into the bump node and not the height output? is that a common thing to do?

edit: seems to be a precision issue with the way i tried to cheat my way out of mirroring around the 0 point. Iā€™ll see what i can do about solving it properly

edit2: Pushed an update (0.1.3) to github, should be fixed now CarlG!

1 Like

Thanks. Initial test seem to have fixed the problem. I thought I managed to mess up the re-installation, the version number within still reads 0.0.1.1 :slight_smile:

No, probably not common :slight_smile: But I wanted to try to replace my own setup with yours, and Iā€™m basing all my height stuff outside the group derived from the UVs. Then something got messed up. I tried the other outputs as well and found all of them to have the same issue. I used the UV output to show that the error was probably in the very core of the setup, where UVs are defined.

Thanks. Iā€™ll look into that. Iā€™m just learning Cycles, so it may take a while.

And the mask connectionā€¦ ah, yes. The tile mask connector. Got it. Thanks again.

Forgot to bump the version number, itā€™s my first add-on, learning as i go :slight_smile: pushed an update for it sorry!

Hmm, whenever I add a node from the new menu, I also get that node added into my regular group, as well as any subgroups used. Adding a secondary of same main in addition seems to add a new dupe of the subgroup (something.001 etc). Is there any way of preventing this from happening? My own rather long function library gets completely messed up. Even if I usually add them using the search function, sometimes I scroll through it looking for something that might be appropriate for what Iā€™m trying to do.


Sadly there is no .hide property on nodegroups, so as much as iā€™d like to, i really canā€™t

you can add it! itā€™s actually very easy.
Itā€™s something Iā€™ve forgot to include in the scripts for Cycles nodes, but it is planned.

This adds the property to ShaderNodeTrees

bpy.types.ShaderNodeTree.is_hidden = BoolProperty(name="is_hidden", description="If the node tree is hidded from the AddMenu", default=False)

And to actually hide you need to change the items generator in the NODE_MT_category_SH_NEW_GROUP and NODE_PT_category_SH_NEW_GROUP classes.
hereā€™s the generator i use (itā€™s the same as in the nodeitems_builtins.py, but with a check for the .is_hidden):


def node_group_items(context):
    if context is None:
        return
    space = context.space_data
    if not space:
        return
    ntree = space.edit_tree
    if not ntree:
        return

    yield NodeItemCustom(draw=group_tools_draw)

    def contains_group(nodetree, group):
        if nodetree == group:
            return True
        else:
            for node in nodetree.nodes:
                if node.bl_idname in node_tree_group_type.values() and node.node_tree is not None:
                    if contains_group(node.node_tree, group):
                        return True
        return False

    for group in context.blend_data.node_groups:
        if group.bl_idname != ntree.bl_idname:
            continue
        # filter out recursive groups
        if contains_group(group, ntree):
            continue
        # filter hidden groups
        if group.is_hidden:
            continue
        yield NodeItem(node_tree_group_type[group.bl_idname],
                       group.name,
                       {"node_tree": "bpy.data.node_groups[%r]" % group.name})

wow! hey!
waitā€¦ is there any chance to set hidden state for a group in the UI?

Today I learn that you can make your own nodes with python.
How do you do that? Is there any good learning material out there somewhere? I tried searching but didnĀ“t find anything and I am not sure what I have to search for.

Yeah i didnā€™t know either until Secrop send me a small sample script, thereā€™s not that much to it, you pretty much create a nodegroup and then hook it up to some ui with some python. donā€™t think thereā€™s any tutorials on it, best just to jump in the deep end and mess with some sample code. Hereā€™s a relatively simple one that makes a mix node for cycles with unlimited inputs.

bl_info = {
    "name": "PicSwitch",
    "author": (
        "LazyDodo"
     ),
    "version": (0, 0, 1,0),
    "blender": (2, 7, 8),
    "location": "Nodes > Add nodes",
    "description": "Pic Switch Cycles Node",
    "warning": "",
    "wiki_url": "",
    "category": "Node"} 


import bpy,nodeitems_utils
from nodeitems_utils import NodeCategory, NodeItem




class ShaderNode_picswitch(bpy.types.NodeCustomGroup):
    bl_name='ShaderNode_picswitch'
    bl_label='pic_switch'
    bl_icon='NONE'
    def treename(self):
        return self.name + '_node_tree_'+str(self.inputCount)+str(self.clamp)
        
    def mypropUpdate(self, context):
        self.getNodetree(self.treename())
        return None
    
    inputCount = bpy.props.IntProperty(name = "Inputs" , default=3, min=2, soft_max=15, update=mypropUpdate) 
    clamp = bpy.props.BoolProperty(name="Clamp", update=mypropUpdate)
    def init(self, context):
        self.inputCount=3
        self.getNodetree(self.treename())
    
    def draw_buttons(self, context, layout):
        layout.label("Node settings")
        row = layout.row(align=True)
        row.alignment = 'EXPAND'
        row.prop(self, "inputCount")
        row = layout.row(align=True)
        row.alignment = 'EXPAND'
        row.prop(self, "clamp")


    def value_set(self, obj, path, value):
        if '.' in path:
            path_prop, path_attr = path.rsplit('.', 1)
            prop = obj.path_resolve(path_prop)
        else:
            prop = obj
            path_attr = path
        setattr(prop, path_attr, value)


    def createNodetree(self, name, count) :
        self.Tmpnode_tree = bpy.data.node_groups.new(name, 'ShaderNodeTree')
        #Nodes
        self.addNode('NodeGroupInput', { 'name':'GroupInput'  })
        self.addNode('NodeGroupOutput', { 'name':'GroupOutput'  })    
        self.addNode('ShaderNodeMath', { 'name':'LimitMin' ,'inputs[1].default_value':0 ,'operation':'MAXIMUM' })
        self.addNode('ShaderNodeMath', { 'name':'LimitMax' ,'inputs[1].default_value':count-1 ,'operation':'MINIMUM' })
        self.addNode('ShaderNodeMath', { 'name':'SubFactor' ,'inputs[1].default_value':1.000001 ,'operation':'MODULO' })
        if (self.clamp):
            print("clamp on!")
            self.addNode('ShaderNodeMath', { 'name':'Clamp1' ,'inputs[1].default_value':0.5 ,'operation':'SUBTRACT' })
            self.addNode('ShaderNodeMath', { 'name':'Clamp2' ,'operation':'ROUND' })
        else:
            print("clamp off!")
        for i in range(1,count):
            nodeName = 'mix'+str(i)+str(i+1)
            self.addNode('ShaderNodeMixRGB', { 'name':nodeName })
        for i in range(1,count-1):
            self.addNode('ShaderNodeMath', { 'name':'Switch'+str(i) ,'inputs[1].default_value':i ,'operation':'GREATER_THAN' })
            self.addNode('ShaderNodeMixRGB', { 'name':'SwitchMix'+str(i) })
        #Sockets
        self.addSocket(False, 'NodeSocketFloat', 'Factor')
        for i in range(1,count+1):
            self.addSocket(False, 'NodeSocketColor', 'Input'+str(i))
        self.addSocket(True, 'NodeSocketColor', 'Color')
        #Links
        if (self.clamp):
            self.innerLink('nodes["GroupInput"].outputs[0]', 'nodes["LimitMin"].inputs[0]')
            self.innerLink('nodes["LimitMin"].outputs[0]', 'nodes["LimitMax"].inputs[0]')
            self.innerLink('nodes["LimitMax"].outputs[0]', 'nodes["Clamp1"].inputs[0]')
            self.innerLink('nodes["Clamp1"].outputs[0]', 'nodes["Clamp2"].inputs[0]')
            self.innerLink('nodes["Clamp2"].outputs[0]', 'nodes["SubFactor"].inputs[0]')
        else:
            self.innerLink('nodes["GroupInput"].outputs[0]', 'nodes["LimitMin"].inputs[0]')
            self.innerLink('nodes["LimitMin"].outputs[0]', 'nodes["LimitMax"].inputs[0]')
            self.innerLink('nodes["LimitMax"].outputs[0]', 'nodes["SubFactor"].inputs[0]')
        for i in range(1,count):
            nodeName = 'mix'+str(i)+str(i+1)
            self.innerLink('nodes["SubFactor"].outputs[0]', 'nodes["' + nodeName +'"].inputs[0]')
            self.innerLink('nodes["GroupInput"].outputs['+ str(i) +']', 'nodes["' + nodeName +'"].inputs[1]')
            self.innerLink('nodes["GroupInput"].outputs['+ str(i+1) +']', 'nodes["' + nodeName +'"].inputs[2]')
        LastNode = 'mix12'
        for i in range(1,count-1):
            self.innerLink('nodes["LimitMax"].outputs[0]', 'nodes["Switch'+str(i)+'"].inputs[0]')
            nodeName = 'mix'+str(i+1)+str(i+2)
            self.innerLink('nodes["Switch' +str(i)+'"].outputs[0]', 'nodes["SwitchMix' + str(i) +'"].inputs[0]')
            self.innerLink('nodes["' + LastNode +'"].outputs[0]', 'nodes["SwitchMix' + str(i) +'"].inputs[1]')
            self.innerLink('nodes["' + nodeName +'"].outputs[0]', 'nodes["SwitchMix' + str(i) +'"].inputs[2]')
            LastNode = 'SwitchMix' + str(i)
        self.innerLink('nodes["' + LastNode +'"].outputs[0]', 'nodes["GroupOutput"].inputs[0]')                    
        self.node_tree = self.Tmpnode_tree
         
    def getNodetree(self, name):
        if bpy.data.node_groups.find(name)==-1:
            self.createNodetree(name,self.inputCount)
        else:
            self.node_tree=bpy.data.node_groups[name]
                   
    def addSocket(self, is_output, sockettype, name):
        #for now duplicated socket names are not allowed
        if is_output==True:
            if self.Tmpnode_tree.nodes['GroupOutput'].inputs.find(name)==-1:
                socket=self.Tmpnode_tree.outputs.new(sockettype, name)
        elif is_output==False:
            if self.Tmpnode_tree.nodes['GroupInput'].outputs.find(name)==-1:
                socket=self.Tmpnode_tree.inputs.new(sockettype, name)
        return socket
       
    def addNode(self, nodetype, attrs):
        node=self.Tmpnode_tree.nodes.new(nodetype)
        for attr in attrs:
            self.value_set(node, attr, attrs[attr])
        return node
   
    def getNode(self, nodename):
        if self.Tmpnode_tree.nodes.find(nodename)>-1:
            return self.Tmpnode_tree.nodes[nodename]
        return None
   
    def innerLink(self, socketin, socketout):
        SI=self.Tmpnode_tree.path_resolve(socketin)
        SO=self.Tmpnode_tree.path_resolve(socketout)
        self.Tmpnode_tree.links.new(SI, SO)
       
    def free(self):
        if self.Tmpnode_tree.users==1:
            bpy.data.node_groups.remove(self.Tmpnode_tree, do_unlink=True)
 
  
class ExtraNodesCategory(NodeCategory):
    @classmethod
    def poll(cls, context):
        return (context.space_data.tree_type == 'ShaderNodeTree' and
                context.scene.render.use_shading_nodes)
 
node_categories = [
    ExtraNodesCategory("SH_PIC_SWITCH", "PicSwitch", items=[
        NodeItem("ShaderNode_picswitch")
        ]),
    ]
      
def register():
    bpy.utils.register_class(ShaderNode_picswitch)
    nodeitems_utils.register_node_categories("SH_PIC_SWITCH", node_categories)
 
 
def unregister():
    nodeitems_utils.unregister_node_categories("SH_PIC_SWITCH")
    bpy.utils.unregister_class(ShaderNode_picswitch)
if __name__ == "__main__":
    register()
   

Itā€™s possible to add a panel somewhere and expose the is_hidden property (for a good interface one may also build a UIlist with all nodegroups, etc), but the straightforward option is to use the Outlinerā€¦


@Lumpengnom, Iā€™ve some scattered information here in the forum, and some in the BlenderSEā€¦
for example, this thread has some usefull infoā€¦ Same here.

comā€™on! I went there in the datablock andi mined my way throughā€¦ but I didnā€™t find it!
thanks a lot sir