Add-ons and more

Upgrading Blender add-ons from 2.79 to 2.80, part 3

While porting my add-ons from Blender 2.79 to 2.80 I encounter all sorts of compatibility issues that I report on from time to time in this blog. Some of those of issues are minor but others go deeper and might take quite some effort to fix.

In previous articles I listed a fair number of issues (1, 2) but the good news is that the number of new issues that pop up is diminishing. Last week's batch is rather small:


  • context.user_preferences becomes context.preferences. That is fairly easy to fix but unfortunately this could break stored preferences as well since they might contain references to this attribute in the set itself (A stored preference or preset is executable Python code that basically sets all sorts of variables)


  • BVHTree.fromobject and related functionality was broken but is now fixed. Thanks to very rapid follow up by Blender dev Bastien Montagne! Note that even though the bug is fixed, the signature of this function (and related ones) has changed: It no longer takes a Scene argument but a Depsgraph argument.


  • context.area.header_text_set(None) must be used instead of context.area.header_text_set("") to restore the default menu. This is not a big thing but if you forget you get an empty taskbar.


IDMapper ported to Blender 2.80


IDMapper has now been ported to Blender 2.80 and is available from BlenderMarket.

There were a fair number of bumps along the road with regard to some parts of the Python API so be aware that Blender 2.80 is still in beta and you might still encounter some difficulties!

Blender add-on: mesh to heightmap

Sometimes it would be convenient to convert a mesh to a heightmap. If you would have a heightmap available, you could flatten your mesh geometry and add a displacement modifier or material with micro displacement instead.

This has many possibilities, including post processing the heightmap in an external program or using adaptive subdivision to reduce geometric complexity where it isn't needed. Therefore I created a small add-on that does just that: Given a mesh that is uv-mapped it creates a new image which contains the z coordinates as grey-scale values.


(original mesh on the left, flattened mesh with map added via a displacement modifier on the right)


(a uv-mapped plane with a single face plus an adaptive subdivision modifier)

Limitations

The add-on will map the object z-coord to a grey-scale value, so this will only work properly on rectangular landscapes, not for example spherical ones. The whole range of z-coordinates present will be mapped to the range [0,1].
You can generate any size map you like, even non-square ones, but the map should be smaller than twice the number of vertices in any dimension, and if you select a map size larger than the number of vertices in a dimension you should check the interpolate option to avoid black lines.
For example, if you have generated a 128 x 128 landscape, you can generate a 128 x 128 map, or any smaller one, but for bigger ones up to 256x256 you need to check the interpolate option.
If you need even bigger maps, generate one that fits will and resize in an external program like Gimp.

The add-on is not super robust and will fail if the mesh has no active uv-map and also when the uv-coordinates are outside the range [0,1] (which is perfectly legal but either scale your uv map before generating the heightmap or adapt the add-on)

Relevant bits of code

For those interested in the inner workings: 

  • line 05: create a new image
  • line 08: initialize the image to an opaque all black
  • line 11: get all uv coordinates from the active uv-layer into a numpy array
  • line 16: get all vertex coordinates into a numpy array
  • line 21: get all vertex indices from the loops. Note that loops and uv-coordinates are aligned, i.e. eery loop has a uv coordinate in a uv-layer
  • line 24: scale/map the z component of the coordinates to the range [0,1]
  • line 31: map uv-coordinates to image pixel coordinates
  • line 35: for every uv value and height, assign the corresponding pixel rgb value. We do not change the alpha. Note that we do this for every loop, not just for every vertex, so this is very inefficient: a typical all-quad mesh contains four loops for every vertex! However, this is fast enough in practice so I didn't bother to optimize
  • line 38: optional interpolation. We determine the missing pixel coordinates and then interpolate those values from the immediate neighbors. 
  • line 50: assign the calculated grey-scale values to the pixels. Note that copying a complete array like we do here is rather fast but would we have done it by indexing the pixels attribute pixel by pixel this would have been prohibitively slow.

 def execute(self, context):
  mesh = context.active_object.data
  width, height = self.width, self.height

  im = bpy.data.images.new("Heightmap",
                    height, width, float_buffer=True)

  hm = np.zeros((width, height, 4), dtype=np.float32)
  hm[:,:,3] = 1.0

  uvlayer = mesh.uv_layers.active.data
  uv = np.empty(len(uvlayer)*2, dtype=np.float32)
  uvlayer.foreach_get('uv',uv)
  uv.shape = -1,2

  co = np.empty(len(mesh.vertices)*3, dtype=
  np.float32)
  mesh.vertices.foreach_get('co', co)
  co.shape = -1,3

  vi = np.empty(len(mesh.loops), dtype=np.int32)
  mesh.loops.foreach_get('vertex_index',vi)

  z = co[:,2][vi]
  zmax = np.max(z)
  zmin = np.min(z)
  zd = zmax - zmin
  z -= zmin
  z /= zd

  uv[:,0] *= height-1
  uv[:,1] *= width-1
  uv = np.round(uv).astype(np.int32)

  for h,xy in zip(z,uv):
   hm[xy[1],xy[0],:3] = h

  if self.interpolate:
   uniq0 = np.unique(uv[:,0])
   uniq1 = np.unique(uv[:,1])
   missing0 = np.setdiff1d(np.arange(height, 
                             dtype=np.int32), uniq0)
   missing1 = np.setdiff1d(np.arange(height,
                             dtype=np.int32), uniq1)
   for y in missing1:
    hm[y,:] = (hm[y-1,:] + hm[y-1,:])/2
   for x in missing0:
    hm[:,x] = (hm[:,x-1] + hm[:,x+1])/2

  im.pixels[:] = hm.flat[:]

Availability

The add-on is available from my GitHub repository (right click on link and select Save As ... or equivalent for your browser). After installing and enabling the add-on, a new menu item Object -> Mesh2Heightmap will be available in the 3d-view in object mode.

Upgrading Blender add-ons from 2.79 to 2.80, part 2

In the first part of this series on porting add-ons from 2.79 to 2.80 I promised to keep notes on issues that might pop-up so here is my second batch:


  • scene.objects.active is no longer available. Use context.active_object. This is due to working with collections,
  • no bl_category, so you cannot decide where your Panel will be located in the Toolbar. This is too bad, but maybe I don understand this completely,
  • the bgl module does not support direct mode at all (not even as an option). This hurts really bad. Yes the new , modern OpenGL capabilities are what we all wished for but porting the old stuff to the new is far from trivial,
  • BVHTree.FromObject() expects a Depsgraph not a scene. This is a breaking change. The relevant Depsgraph is available from context.depsgraph,
  • scene.ray_cast() expects a view_layer to determine what is visible in the scene. Again a breaking change related to the previous one. The relevant view_layer is available from context.window.view_layer,
  • Lamps are now called Lights, this affects object.type ('LIGHT') as well as bpy.data.lights,
  • Color() constructor always expects 3 values, but the .color attribute in a vertex color layer is 4 items (RGBA). Like the next item on this list this might have been present in 2.79 already but just comes to light now that I am porting add-ons,
  • The name of a module may not contain -  (i.e. a dash or minus sign) or install will silently fail (might have been the case in 2.79 too)

WeightLifter ported to Blender 2.80


WeightLifter has now been ported to Blender 2.80 and is available from BlenderMarket.
Porting this add-on was a bit more work than porting NodeSet Pro, so be aware that Blender 2.80 is still in beta and you might still encounter some difficulties!

NodeSet Pro ported to Blender 2.80


NodeSet Pro has now been ported to Blender 2.80 and is available from BlenderMarket.
Porting this add-on has been fairly easy but be aware that Blender 2.80 is still in beta so you might encounter difficulties!

Upgrading Blender add-ons from 2.79 to 2.80

i have started research into upgrading my (commercial) add-ons from 2.79 to 2.80. in documenting what i encounter, i hope i can spare people some frustrations and learn something about add-ons in general. i didn't start earlier, partly because i had some other stuff to do but mainly because the Python API needed to be finalized and more or less stable: with many add-ons to consider for an upgrade starting when everything is still in flux would run the risk of wasting a lot of time.

Expectations

the Blender developers provided some documentation in the API changes in in a short document. Unfortunately, quite a few of them are breaking changes, i.e. the have to be changed in order to work at all. Many of these changes impact every add-on, like no longer having a register_module() utility function while some others apply mainly to math heavy add-ons, like the use of the @ operator for matrix multiplication. Unfortunately quite a few of my add-ons are math heavy so i will probably need to spend quite some time on this.

Annoyances

the Blender devs decided it was a good idea to force all keyword parameters to actually use those keywords. And indeed this will prevent accidentally using a positional argument as a keyword argument and aid in readability. This is a Good Thing™. However it is bloody annoying that layout.label() is affected by this, which I use a lot which just a single text argument ...
The price for biggest annoyance however goes to the mandatory change that forces class variables that are initialized with a call to some sort of property function like an Operator with for example a size = FloatProperty(name="size") property, to a type annotation like size: FloatProperty(name="size")
A small change it may seem but properties are everywhere so it is a lot of work. And although i read the PEP thoroughly, I fail to see the advantage. I'd rather hoped the devs would concentrate on things that change because of Blender's new structure, like collections and viewport related things, than on this new Python fad ...

Upcoming

I am just starting the research on converting my add-ons but you can expect a couple of articles the next couple of months with hopefully some useful findings.