Jump to content
Ultimaker Community of 3D Printing Experts

Plugin for woodFill or similar filament

Recommended Posts

Does anyone know if there is a plugin for Cura (I'm currently using 2.1.2) that can automate a random heat change pattern, resulting in a colour change when using woodFill or a similar filament? I know there is the TweakAtZ 5.0.1 script, but this can be a bit laborious when you are printing a tall object and you want the colour to change every millimetre or so.

Share this post

Link to post
Share on other sites

Check out this item on Thingiverse. The author has a standalone python script that it looks like you just feed it your .gcode file

Generate wood patterns with temperature changes

This might work as-is in Cura 2.x, I dunno


Thanks for that DaHai8,

I had already found that, but can't work out how to plug it in to the Cura software. So if anyone can help with that I'd appreciate it.

Share this post

Link to post
Share on other sites

This one had to be rehashed from top/bottom because it tried to be a 15.x plugin and a stand-alone python script at the same time.

There are new defs in Cura 2.x like "getSettingDataString(self)" to get the user's input, as well as 15.x handled the .gcode file directly whereas in 2.x, the plugin is handed a structure containing the gcode, so no file reads/writes.

There are also some Python2 v. new Python3 differences that caught me a few times.

However, as for Cura 2.1.x v 2.3 the changes are minor (which is why I kept missing them!):

1. getSettingData appears to be deprecated and replaced with getSettingDataString

2. "default" is now "default_value" in the settings

3. "visible" is deprecated and cannot be used

4. "name", "key", "metadata", "version" in the getSettingDataString appear to be required

5. "settings" is also required, but it can be empty brackets

6. You can return the Setting Data as a big string (or not - not sure which way is preferred):


def getSettingDataString(self):       return """{           "name":"Layer Numbers",           "key": "LayerNumbers",           "metadata":{},           "version": 2,           "settings":           {           }       }"""


Everything else seems to work without modifications. So its all apparently confined to the getSettingDataString function/def/thingy

Share this post

Link to post
Share on other sites

Wood Grain Post Processing Plugin for Cura 2.3 (Beta) is done!

I'm don't think it will work in 2.1.x...

Here's the code WoodGrain.py (no warranty whatsoever, use at your own risk, etc., etc., blah, blah, blah...)


#Name: Wood Grain#Info: Vary the print temperature troughout the print to create wood rings with some printing material such as the LayWoo. The higher the temperature, the darker the print.#Depend: GCode#Type: postprocess#Param: minTemp(float:180) Mininmum print temperature (degree C)#Param: maxTemp(float:230) Maximun print temperature (degree C)#Param: grainSize(float:3.0) Average "wood grain" size (mm)#Param: firstTemp(float:0) Starting temperature (degree C, zero to disable)#Param: spikinessPower(float:1.0) Relative thickness of light bands (power, >1 to make dark bands sparser)#Param: maxUpward(float:0) Instant temperature increase limit, as required by some firmwares ©#Param: zOffset(float:0) Vertical shift of the variations, as shown at the end of the gcode file (mm)## Original Author: Jeremie Francois (jeremie.francois@gmail.com)# License: GNU Affero General Public License http://www.gnu.org/licenses/agpl.html# Heavily Modified by: Zhu Da Hai. Sept 2, 2016 (appologies to Jeremie)# Compatible with: Cura 2.3 Beta#from ..Script import Scriptimport reimport randomimport mathimport timeimport datetimeeol = "\n"import inspectimport sysimport getoptclass WoodGrain(Script):   def __init__(self):       super().__init__()   def getSettingDataString(self):       return {           "name":"Wood Grain",           "key": "WoodGrain",           "metadata":{},           "version": 2,           "settings":           {               "a_minTemp":                {                   "label": "Min Temp",                   "description": "Mininmum print temperature (degree C)",                   "unit": "c",                   "type": "float",                   "default_value": 180               },               "b_maxTemp":                {                   "label": "Max Temp",                   "description": "Maximun print temperature (degree C)",                   "unit": "c",                   "type": "float",                   "default_value": 230               },               "c_firstTemp":                {                   "label": "First Temp",                   "description": "Starting temperature (degree C, zero to disable)",                   "unit": "c",                   "type": "float",                   "default_value": 0               },               "d_grainSize":                {                   "label": "Grain Size",                   "description": "Average 'wood grain' size (mm)",                   "unit": "mm",                   "type": "float",                   "default_value": 3.0               },               "e_maxUpward":                {                   "label": "Max Upward",                   "description": "Instant temperature increase limit, as required by some firmwares ©",                   "unit": "c",                   "type": "float",                   "default_value": 0               },               "f_zOffset":                {                   "label": "Z-Offset",                   "description": "Vertical shift of the variations, as shown at the end of the gcode file (mm)",                   "unit": "mm",                   "type": "float",                   "default_value": 1.0               },               "g_randomSeed":                {                   "label": "Random Seed",                   "description": "Seed for Random Number Generator (0 = no seed - randomize)",                   "unit": "",                   "type": "int",                   "default_value": 0               },               "h_spikinessPower":                {                   "label": "Spikiness Power",                   "description": "Relative thickness of light bands (power, >1 to make dark bands sparser)",                   "unit": "",                   "type": "float",                   "default_value": 1.0               }           }       }   def getValue(self, line, key, default = None):       if not key in line or (';' in line and line.find(key) > line.find(';')):           return default       sub_part = line[line.find(key) + len(key):]       m = re.search('^[-+]?[0-9]+\.?[0-9]*', sub_part)       if m is None:           return default       try:           return float(m.group(0))       except:           return default   def getZ(self, line, default = None):       # new 20130727: now support G0 in addition to G1       if self.getValue(line, 'G') == 0 or self.getValue(line, 'G') == 1:           return self.getValue(line, 'Z', default)       else:           return default   try:       xrange   except NameError:       xrange = range   def perlinToNormalizedWood(self, z, zOffset, grainSize, spikinessPower, perlin):       banding = 3       octaves = 2       persistence = 0.7       noise = banding * perlin.fractal(octaves, persistence, 0,0, (z+zOffset)/(grainSize*2));       noise = (noise - math.floor(noise)) # normalized to [0,1]       noise= math.pow(noise, spikinessPower)       return noise   def noiseToTemp(self, noise, maxTemp, minTemp):       return minTemp + noise * (maxTemp - minTemp)   class Perlin:       # Perlin noise: http://mrl.nyu.edu/~perlin/noise/       def __init__(self, tiledim=256):           self.tiledim= tiledim           self.perm = [None]*2*tiledim           permutation = []           # xrange changed to range for Python3           for value in range(tiledim):               permutation.append(value)           random.shuffle(permutation)           # xrange changed to range for Python3           for i in range(tiledim):               self.perm[i] = permutation[i]               self.perm[tiledim+i] = self.perm[i]       def fade(self, t):           return t * t * t * (t * (t * 6 - 15) + 10)       def lerp(self, t, a, b):           return a + t * (b - a)       def grad(self, hash, x, y, z):           #CONVERT LO 4 BITS OF HASH CODE INTO 12 GRADIENT DIRECTIONS.           h = hash & 15           if h < 8: u = x           else:     u = y           if h < 4: v = y           else:               if h == 12 or h == 14: v = x               else:                  v = z           if h&1 == 0: first = u           else:        first = -u           if h&2 == 0: second = v           else:        second = -v           return first + second       def noise(self, x,y,z):           #FIND UNIT CUBE THAT CONTAINS POINT.           X = int(x)&(self.tiledim-1)           Y = int(y)&(self.tiledim-1)           Z = int(z)&(self.tiledim-1)           #FIND RELATIVE X,Y,Z OF POINT IN CUBE.           x -= int(x)           y -= int(y)           z -= int(z)           #COMPUTE FADE CURVES FOR EACH OF X,Y,Z.           u = self.fade(x)           v = self.fade(y)           w = self.fade(z)           #HASH COORDINATES OF THE 8 CUBE CORNERS           A = self.perm[X  ]+Y; AA = self.perm[A]+Z; AB = self.perm[A+1]+Z           B = self.perm[X+1]+Y; BA = self.perm[b]+Z; BB = self.perm[b+1]+Z           #AND ADD BLENDED RESULTS FROM 8 CORNERS OF CUBE           return self.lerp(w,self.lerp(v,                   self.lerp(u,self.grad(self.perm[AA  ],x  ,y  ,z  ), self.grad(self.perm[bA  ],x-1,y  ,z  )),                   self.lerp(u,self.grad(self.perm[AB  ],x  ,y-1,z  ), self.grad(self.perm[bB  ],x-1,y-1,z  ))),               self.lerp(v,                   self.lerp(u,self.grad(self.perm[AA+1],x  ,y  ,z-1), self.grad(self.perm[bA+1],x-1,y  ,z-1)),                   self.lerp(u,self.grad(self.perm[AB+1],x  ,y-1,z-1), self.grad(self.perm[bB+1],x-1,y-1,z-1))))       def fractal(self, octaves, persistence, x,y,z, frequency=1):           value = 0.0           amplitude = 1.0           totalAmplitude= 0.0           # xrange changed to range for Python3           for octave in range(octaves):               n= self.noise(x*frequency,y*frequency,z*frequency)               value += amplitude * n               totalAmplitude += amplitude               amplitude *= persistence               frequency *= 2           return value / totalAmplitude   def execute(self, data):       minTemp = float(self.getSettingValueByKey("a_minTemp"))       maxTemp = float(self.getSettingValueByKey("b_maxTemp"))       firstTemp = float(self.getSettingValueByKey("c_firstTemp"))       grainSize = float(self.getSettingValueByKey("d_grainSize"))       maxUpward = float(self.getSettingValueByKey("e_maxUpward"))       zOffset = float(self.getSettingValueByKey("f_zOffset"))       randomSeed = int(self.getSettingValueByKey("g_randomSeed"))       spikinessPower = float(self.getSettingValueByKey("h_spikinessPower"))       myStr = ""       if randomSeed != 0:           random.seed( randomSeed )       # new 20130727: limit the number of changes for helicoidal/Joris slicing method       minimumChangeZ=0.1       perlin = WoodGrain.Perlin()       # Generate normalized noises, and then temperatures (will be indexed by Z value)       noises = {}       # first value is hard encoded since some slicers do not write a Z0 at the first layer!       # TODO: keep only Z changes that are followed by real extrusion (ie. discard non-printing head movements!)       noises[0] = self.perlinToNormalizedWood(0, zOffset, grainSize, spikinessPower, perlin)       pendingNoise = None       formerZ = -1       for layer in data:           lines = layer.split("\n")           for line in lines:               thisZ = self.getZ(line, formerZ)               if thisZ > 2 + formerZ:                   formerZ = thisZ                   #noises = {} # some damn slicers include a big negative Z shift at the beginning, which impacts the min/max range               elif abs ( thisZ - formerZ ) > minimumChangeZ:                   formerZ = thisZ                   noises[thisZ] = self.perlinToNormalizedWood(thisZ, zOffset, grainSize, spikinessPower, perlin);       lastPatchZ = thisZ; # record when to stop patching M104, to leave the last one switch the temperature off       # normalize built noises       noisesMax = noises[max(noises, key = noises.get )]       noisesMin = noises[min(noises, key = noises.get )]       for z,v in noises.items():           noises[z]= (noises[z]-noisesMin)/(noisesMax-noisesMin)       #       # new 20130727: header and first (blocking) temperature change       #       warmingTempCommands="M230 S0" + eol # enable wait for temp on the first change       if firstTemp == 0:           warmingTempCommands+= ("M104 S%i" + eol) % self.noiseToTemp(0, maxTemp, minTemp)       else:           warmingTempCommands+= ("M104 S%i" + eol) % firstTemp       # The two following commands depends on the firmware:       warmingTempCommands+= "M230 S1" + eol # now disable wait for temp on the first change       warmingTempCommands+= "M116" + eol # wait for the temperature to reach the setting (M109 is obsolete)       # Prepare a transposed temperature graph for the end of the file       graphStr=";WoodGraph: Wood temperature graph (from "+str(minTemp)+"C to "+str(maxTemp)+"C, grain size "+str(grainSize)+"mm, z-offset "+str(zOffset)+")"       if maxUpward:           graphStr+=", temperature increases capped at "+str(maxUpward)       graphStr+=":"       graphStr+=eol       thisZ = -1       formerZ = -1       warned = 0       header = 1       savelayer = 0       postponedTempDelta=0 # only when maxUpward is used       postponedTempLast=None # only when maxUpward is used       skiplines=0       # Now Modify the gCode       for layer in data:           if header == 1:               index = data.index(layer)                layer = warmingTempCommands + layer               data[index] = layer #Override the data of this layer with the modified data               header = 0           lines = layer.split("\n")           for line in lines:               if "; set extruder " in line.lower(): # special fix for BFB                   index = data.index(layer)                    layer = layer.replace(line,line + "\n" + warmingTempCommands)                   warmingTempCommands=""                   savelayer = 1               elif skiplines > 0:                   skiplines= skiplines-1;               elif ";woodified" in line.lower():                   skiplines=4 # skip 4 more lines after our comment               elif not ";woodgraph" in line.lower(): # forget optional former temp graph lines in the file                   if thisZ == lastPatchZ:                       line = line # dummy                   elif not "m104" in line.lower(): # forget any previous temp in the file                       thisZ = self.getZ(line, formerZ)                       if thisZ != formerZ and thisZ in noises:                           if firstTemp != 0 and thisZ<=0.5: # if specifed, keep the first temp for the first 0.5mm                               temp= firstTemp                           else:                               temp= self.noiseToTemp(noises[thisZ], maxTemp, minTemp)                               # possibly cap temperature change upward                               temp += postponedTempDelta;                               #print("ppdelta=%f\n" % postponedTempDelta)                               postponedTempDelta = 0                               if postponedTempLast!= None and maxUpward > 0 and temp > postponedTempLast + maxUpward:                                   postponedTempDelta = temp - (postponedTempLast + maxUpward)                                   temp= postponedTempLast + maxUpward                               if temp > maxTemp:                                   postponedTempDelta= 0                                   temp= maxTemp                               postponedTempLast = temp                               index = data.index(layer)                                layer = layer.replace(line,line + "\n" + ("M104 S%i ; Wood Grain" + eol) % temp)                               savelayer = 1                           formerZ = thisZ                           # Build the corresponding graph line                           #t = int(19 * noises[thisZ])                           t= int(19 * (temp-minTemp)/(maxTemp-minTemp))                           myStr = ";WoodGraph: Z %03f " % thisZ                           myStr += "@%3iC | " % temp                           # xrange changed to range for Python3                           for i in range(0,t):                               myStr += "#"                           # xrange changed to range for Python3                           for i in range(t+1,20):                               myStr += "."                           graphStr += myStr + eol           if savelayer == 1:               data[index] = layer               savelayer = 0       index = data.index(layer)       layer = layer + graphStr + eol       data[index] = layer       return data    


  • Like 2

Share this post

Link to post
Share on other sites

To use DaHai8's plugin on a mac:

Step 1) Download the plugin here: http://bitman.org/dahai/WoodGrain.zip

Step 2) Unzip and place WoodGrain.py in the following dir:


Step 3) Re-Launch Cura and select:

Extensions --> Post Processing --> Modify G-Code

Step 4) Click "Add a Script" --> "Wood Grain"

Step 5) ¯\_(ツ)_/¯

Step 6) Profit

Share this post

Link to post
Share on other sites


I just tried to use this script and had some troubles.

I am on Windows with Cura 2.6.2 and able to use the plugin, but only the 2 first layers (layer 0 and layer 1) are affected by the temp changes after postprocessing.

After layer 1, all the rest of code stays at the same temp and I don't understand why.

I tried different wood grain paramaters, but it is still the same.

Thanks for helping

Share this post

Link to post
Share on other sites

I have that bug fixed and its posted on my site for anyone to pick up

Cura Post Processing Plugins

I did uncover another bug that I haven't had time to fix: sometimes (usually on .2mm layer heights) it will not do any Wood Grain temperature mod on the last few layers, no will it save the Wood Grain Graph at the end of the gcode file.

I'll try to get those fixed soon, but I don't consider them critical at the moment.

Share this post

Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now

  • Our picks

    • Taking Advantage of DfAM
      This is a statement that’s often made about AM/3DP. I'll focus on the way DfAM can take advantage of some of the unique capabilities that AM and 3DP have to offer. I personally think that the use of AM/3DP for light-weighting is one of it’s most exciting possibilities and one that could play a key part in the sustainability of design and manufacturing in the future.
        • Like
      • 3 replies

Important Information

Welcome to the Ultimaker Community of 3D printing experts. Visit the following links to read more about our Terms of Use or our Privacy Policy. Thank you!