Hidden line renderer / Learning blender python 2.5

Note: Updated link to script added to fipo on request:
http://thomaskrijnen.com/BPY/0.05/hiddenline.py

Hi all,

With the beta just peeked around the corner, I decided it’s time to try and learn the new python api.
The API itself is great, but coming from 2.4x, a lot of questions remain.
I picked a semi-useful and not too complicated objective of a hidden line renderer by using actual geometry.
Just turns every edge into a small tube and we’re done. So this is what it does as of now:


Tubes are placed along every edge. Perspective can be corrected by making tubes wider as they’re further from the active camera.
The profile edges of the mesh (=view dependent) can be made thicker (in a very limited way).

Let me start by just posting the code and then ask some questions I hope some of you can answer.

import bpy, mathutils
from math import *

############
# GUI code #
############

bpy.types.Scene.BoolProperty( attr="hr_enable",\
name="Enable",description="Enable Hiddenline Rendering")
bpy.types.Scene.BoolProperty( attr="hr_joints",\
name="Joints",description="Add joints between cylinders")
bpy.types.Scene.BoolProperty( attr="hr_correct",\
name="Correcr",description="Correct the camera perspective")
bpy.types.Scene.BoolProperty( attr="hr_profile",\
name="Profile",description="Show profile edges")
bpy.types.Scene.FloatProperty( attr="hr_profile_width",\
name="Width",description="Width", min=0.0001, max=1.0, precision=5, default=0.05)
bpy.types.Scene.BoolProperty( attr="hr_edge",\
name="Edge",description="Show regular edges")
bpy.types.Scene.FloatProperty( attr="hr_edge_width",\
name="Width",description="Width", min=0.0001, max=1.0, precision=5, default=0.01)

class ForceUpdate(bpy.types.Operator):
    bl_idname = "forceupdate"
    bl_label = "Update"
    bl_description = "Update the wires on each and every mesh in the scene"
    
    def invoke(self, context, event):
        create_mesh()
        return {'FINISHED'}


class RenderPanel(bpy.types.Panel):
    bl_space_type = "PROPERTIES"
    bl_region_type = "WINDOW"
    bl_context = "render"
    bl_label = "Hidden Line Rendering"

    def draw(self, context):
        layout = self.layout
        rd = context.scene
        row = layout.split(0.25)
        row.prop(rd, "hr_enable", text="Enable")
        if context.scene.hr_enable == True:
            row = row.split(0.33)
            row.prop(rd, "hr_joints", text="Joints")
            row.prop(rd, "hr_correct", text="Correct perspective")
            row = layout.split(0.25)
            row.prop(rd, "hr_profile", text="Profile")
            if context.scene.hr_profile == True:
                row.prop(rd, "hr_profile_width", text="Width")
            row = layout.split(0.25)
            row.prop(rd, "hr_edge", text="Edge")
            if context.scene.hr_edge == True:
                row.prop(rd, "hr_edge_width", text="Width")
        row = layout.row()
        row.operator('forceupdate') 
        
bpy.types.register(ForceUpdate)
bpy.types.register(RenderPanel)
#################
# Mesh creation #
#################

def create_mesh():
    try: bpy.context.scene.objects.unlink(bpy.data.objects['hr_wires'])
    except: pass
    # Store existing objects in scene for later use
    old_obs = [_ob.name for _ob in bpy.data.objects]
    active_cam = bpy.context.scene.camera
    correct_perspective = False
    # ViewProjection matrix
    vp_mat = mathutils.Matrix(active_cam.matrix_world).invert().transpose()
    # Get data from active camera
    if active_cam is not None:
        if active_cam.data.type == 'PERSP':
            correct_perspective = bpy.context.scene.hr_correct
            cam_loc = active_cam.location
            aspect = bpy.context.scene.render.resolution_x / bpy.context.scene.render.resolution_y
            vp_mat *= PerspectiveMatrix(active_cam.data.angle,aspect)
    edge_profile_ratio = bpy.context.scene.hr_profile_width / bpy.context.scene.hr_edge_width
    for ob in bpy.context.scene.objects:
        if ob.type != 'MESH': continue
        data = ob.data
        # Transform vertices into world space
        verts = [ob.matrix_world*_v.co for _v in data.verts]
        # Transform vertices into screen space
        p_verts = [vecxmat(_v,vp_mat) for _v in verts]
        # Calculate widths based on distance to camera
        if correct_perspective: v_widths = [(_v-cam_loc).length * bpy.context.scene.hr_edge_width for _v in verts]
        # Create spheres at verts
        if bpy.context.scene.hr_joints:
            for i in range(0,len(verts)):
                point(verts[i],v_widths[i] if correct_perspective else bpy.context.scene.hr_edge_width)  
        __i = len(data.edges)
        for e in data.edges:
            v1 = verts[e.verts[0]]
            v2 = verts[e.verts[1]]
            faces = [_f for _f in data.faces if e.key in _f.edge_keys]
            # Profile edges are non manifold edges or edges with both faces on the same side
            is_profile = False
            w1 = v_widths[e.verts[0]] if correct_perspective else bpy.context.scene.hr_edge_width
            w2 = v_widths[e.verts[1]] if correct_perspective else False  
            if bpy.context.scene.hr_profile:
                is_profile = len(faces) != 2
                if not is_profile:
                    vp_v1 = vecxmat(v1,vp_mat)
                    vp_v2 = vecxmat(v2,vp_mat)
                    c1 = vecxmat(ob.matrix_world*faces[0].center,vp_mat)
                    c2 = vecxmat(ob.matrix_world*faces[1].center,vp_mat)
                    is_profile = ccw(vp_v1,vp_v2,c1) == ccw(vp_v1,vp_v2,c2)
                if is_profile:
                    w1 *= edge_profile_ratio
                    if w2 != False: w2 *= edge_profile_ratio
            # Function to create geometry
            if bpy.context.scene.hr_edge or is_profile:
                edge(v1,v2,w1,w2)
            
    for ob in bpy.context.scene.objects:
        ob.select = not ob.name in old_obs
    bpy.ops.object.join()
    wire_ob = [_ob for _ob in bpy.context.scene.objects if _ob.select == 1][0]
    wire_ob.name = 'hr_wires'
####################
# Helper functions #
####################

def point(p,w):
    bpy.ops.mesh.primitive_uv_sphere_add(segments=16,rings=16,size=w,location=tuple(p))

def edge(v1,v2,w1,w2):
    if w2 != False:
        width_ratio = w1 / w2
    d = v2 - v1
    l = d.length / 2.0
    d = d.normalize()
    e = mathutils.Vector((0,0,1)) if abs(d.z) < 0.5 else mathutils.Vector((1,0,0))
    f = d.cross(e)
    f = f.normalize()
    e = d.cross(f)
    e.normalize()
    mat = mathutils.Matrix(f,e,d).resize4x4()
    cent = (v1 + v2) / 2.0
    bpy.ops.mesh.primitive_tube_add(radius=w1,depth=l)
    new_mesh = bpy.context.active_object.data
    if w2 != False: 
        for v2 in [_v2 for _v2 in new_mesh.verts if _v2.co.z > 0]:
            v2.co.x /= width_ratio; v2.co.y /= width_ratio
    new_mesh.transform(mat)
    new_mesh.transform(mathutils.TranslationMatrix(cent))

def ccw(A,B,C):
    return (C[1]-A[1])*(B[0]-A[0]) > (B[1]-A[1])*(C[0]-A[0])

def vecxmat(v,m):
    r = mathutils.Vector(v).resize4D() * m
    r /= r.w
    r.z = 0.0
    return (r.x,r.y,r.z)
    
def PerspectiveMatrix(fovx, aspect, near=0.1, far=100.0):
    near = 0.5
    right = tan(fovx * 0.5) * near
    left = -right
    top = float(right / aspect)
    bottom = -top
    return mathutils.Matrix(
      [(2.0 * near)/float(right - left), 0.0, float(right + left)/float(right - left), 0.0],
      [0.0, (2.0 * near)/float(top - bottom), float(top + bottom)/float(top - bottom), 0.0],
      [0.0, 0.0, -float(far + near)/float(far - near), -(2.0 * far * near)/float(far - near)],
      [0.0, 0.0, -1.0, 0.0])

Some questions:

  • Firstly, does anyone know how to add a checkbox onto the panel label itself, as the native panels such as AA and Bake have?

  • Secondly, the script is really sloooww, are there any bottlenecks I should try to avoid?

  • Thirdly, for now the script updates after pressing the update button, how can I make it react to changes of the sliders and checkboxes?

  • (Why isn’t there a PerspectiveMatrix() in mathutils?)

    Any other comments whatsoever will be appreciated.

Thanks a lot

I could not get it to run. Please put in in an attachment, I think it probably broke while copy/paste.


Traceback (most recent call last):
  File "/home/cw/blends/sculpt/HL.blend/solid.py", line 29, in invoke
NameError: global name 'create_mesh' is not defined

Carsten

Maybe it is also a good idea to have only specific meshes handled. E.g. enable it on object base.

Carsten

PS: I just played a bit with “create_mesh()”, did not get it to run completely.

This script should be in the materials menu, so we could render some models with wireframe, and others without it.

Ok, sorry bout that, please download the code here:
http://www.thomaskrijnen.com/BPY/0.02/hiddenline.py

I’ve quickly added the option to enable/disable wires on an individual objects, no tweaking settings per object though, but that would be just as easy. You still need to update in the render tab to actually get some wires. Could also have been based on material, but that would be slightly more work and a little unclear when using meshes with multiple material indices.

The code is slow and buggy at best (please don’t use too complex meshes for now), keep in mind my main objective for this script is to learn the bpy api (selfish as I am), but if in the end something remotely useful pops out, that’d be great.

Works!

Carsten

Hi there!

I am not using very much Blender 2.5… Nevertheless, I have here installed 2.5.2 version for opening some .blend files for 2.5 or running some interesting scripts for 2.5 too :wink:

So I ran your script but it gave me an error on the first run saying there is “No module named mathutils” @ line 1 which I corrected to “Mathutils”. Then on the second run I’ve got another error that Im no possible to correct at present cause I really dont know 2.5 API sufficient enough:

location:<unknown location>:-1
StructRNA "Update" freed while holding a python reference

I dont know how the code was successfully running on other meshes - I was trying it on the default cube which is fairly simple :eek:

I was really curiouc to see the result on the cube - may be interesting and perhaps something I was searching in the past BUT no result for me so far! :frowning:

Anyway… As soon as you report about a prolonged running time at complicated meshes, I tryed to look at the script logic and organisation cause I faced similar problems while coding for 2.4.x. You need to take in account that there are negative effects on the timing that come for Blender Python itself and these CANNOT be avoided. THese are like testing elements from a big list against something (if… else…). As the number of such elements (N) goes up and list are real BIG, the run time goes up too. The bad news here is that relationship is non-linear and the run time goes up much faster than N.

There are things that you can avoid though and effectively lower your script run time. For example, you try at line 108 for each edge from the mesh you iterate ALL mesh faces to find out the faces that use THAT edge. Correct??? BUT here is a classic example of script logic leading to a prolonged run. From one side for BIG meshes you have a GREAT number of edges (N_edges). Normally, about half of N_edges is the number of faces (N_faces)… So you process thousands of times one and the same BIG list of faces plus that you have an IF in the middle = N_edges * N_faces IFs… While you can do that only once and then (when you need the info) you can take it form an appropriate dictionary, for example.

There may be other sections of your script working as bottleneck. The easiest way to find out what are they and how much certain measures (re-coding) are effective in regards to improvement is to check the time before and after the section of interest, then print the difference (t2-t1)… You may do that for the whole script so that you know what is the relative share of each section in the overall script performance and concentrate on improving sections with higher share. From my proactice with Blender Python this gives real nice results :spin:

I think also, one bug of the script is that you are checking if there is an active camera set-up but you dont tell what if NOT such a case!!! (see lines 84-89)… My guess is that you should terminate any further script run in case there is NO cam set-up. :wink:

If you have a version for 2.49, please publish it. Then myself (and other people) may provide more help on polishing it and avoid certain bottlenecks in its run.

Regards,

Thanks a lot for testing Carsten.

@Abidos, thanks a lot for your detailed response. You turned out to be very right about getting the faces by edge, this proved to be one of the major bottlenecks. Also the fact that objects were created for every cylinder which were later joined to a single object was silly. I will look into your active_camera bug, it is definitely a bug, although the script doesn;t have to stop when there isn’t an active camera, it just won’t be able to correct the perspective and determine the profile edges or maybe should use the 3DView matrices for that, I’ll think about that. From your errors I deduce you have to at least run the 2.5.3 beta in order to run the script, sorry for that, if you have some spare time, please upgrade :)!! I’ve tried to port the script back to 2.49, but got hopelessly stuck at the projection matrices. If you want you can have a go with it, it works except fot the profile edges and gui: http://thomaskrijnen.com/BPY/2.49.blend

I’ve updated the code to work better on complex scenes, also there is now the possibility to adjust settings for individual objects. The code runs quite fast now - well as fast as I was able to get it - the subdivided monkey updates instantly, a test scene with 100k verts worth of ngplants takes a few secs to update: http://www.thomaskrijnen.com/BPY/hidden-line-render-3.jpg

Please download the code here: http://www.thomaskrijnen.com/BPY/0.03/hiddenline.py

Well, I tried it in 2.49 and it works OK. :spin:

I tried to develop a script to building an octet by cylinders ONLY but I started my vacation in mid June so the script is unfinished. Some initial results are shown here. I noticed that your cylinders does not join smoothly at all at a vertex - just cross there. May be you wish to work on smooth joining which apparently would involve removing the doubles thus significantly reduce N_verts of the new object (the thick wire-frame).

In my script I used constant radius for the cylinders. In fact, for faster “construction” I’ve produced just one which is duplicated N times where N is the number of edges, then it is rotated and scaled properly to fit each edge processed, then at verts - ends of ALL cylinder need to be processed (“welded”) to look really as being smoothly welded. This may be important at some cases :wink:

At your script - is the

correct_perspective = True

= False/True to switch constant/non-constant cylinder radius? In fact when TRUE, yours are NOT cylinders :cool:

Suggestion - make it work for 1 object or for the selected mesh objects (NOT iterating ALL mesh objects)… This way you would avoid your message in the beginning saying to delete the OLD thick wires prior to running the script. You may also wish to make it produce a different object (thick wires) for each of the objects processed. This may be extremely helpful for users… :cool:

EDIT: Sorry, just forgot that --> Your script needs a Redraw() at end - either in the main program or in your create_mesh() proc… At least in 2.49 version…

Regards,

Abidos, please note that the 2.49 is only a developer preview :wink: the 2.5.3 version is much much nicer, do try it if you find the time :). I was a little hesitant too to delve into coding for 2.5, but the new possibilities for gui integration and overall consistency make coding for 2.5 a lot more rewarding… :slight_smile:

I understand now what your interest is in this subject. And sorry for the disappointment that I used such a hackish way of dealing with the cylinder intersections. But note that at the resolution the wires are intended to be rendered these intersections are not noticeable, especially with the option to place small spheres at the centers.

A few words regarding your problem: with a finite amount of cylinder segments I think there isn’t always a solution for your problem. As I think you know you can find the cut planes by:


    for v in me.verts:
        edges = [ed for ed in me.edges if v.index in ed.key]
        if len(edges) < 2: continue
        for edge1 in edges:
            other1 = edge1.v1 if edge1.v2 == v else edge1.v2
            dir1 = (other1.co - v.co).normalize()
            for edge2 in edges:                
                if edge1 == edge2: continue
                other2 = edge2.v1 if edge2.v2 == v else edge2.v2
                dir2 = (other2.co - v.co).normalize()
                bisec = (dir1+dir2).normalize()
                cross = Blender.Mathutils.CrossVecs(dir1,dir2).normalize()
                
                vec1 = v.co
                vec2 = v.co + bisec
                vec2 = v.co + cross
                # Feed these to Blender.Mathutils.Intersect()

and then use Blender.Mathutils.Intersect() to cut the cylinders off at the closest intersection result found with a ray from the opposing cylinder end.

But unless you use a very large amount of vertices for your cylinders. The cylinders may not always precisely match at there vertices, due to their rotations in order to match edge orientations.

Oh, yes, I understood your way to dealing with cylinders intersection even earlier… Using small spheres is a smart solution rather then my attempt to join them more precisely :wink:

I havent mentioned earlier but my approach to “welding” such cylinders lyes on using my Cutting tool as many times as needed. Here is more info on it… The cutting tool I use is very precise and runs real fast even at its current stage (it may get optimised to working some 30% faster but… errr… Im lazy to do that right now :eek:). Anyway it has no problems like the old versions published prior mine. I will port it to 2.5 only after it is real stable and has more functional API than in 2.49 cause my modules do contain many rows of code to translate. FYI Im not using the manual set-up while cutting cylinders for the octet but virtual ones that are appropriately places for the purpose. Such approach gives that nice result no matter how many edges you have in 1 vert - the only problem is to set-up cutter’s location/rotation in the middle of the spatial angle between each pair of edges. Your suggested approach may mathematically work BUT not in Blender - due to its internal imprecision problems. I.e. you will be difficult to find the exact intersection of 2 edges from 2 different cylinder cause they may NOT intersect mathematically even they are constructed the same way. So you need to ALWAYS work with a threshold in Blender and this will bring you a lot of trouble :eek: Further to that, you would ALWAYS need to distinguish between 2 intersection that you will have for each edge from 1 cylinder that intersects its adjacent. All this may get even slower than cutting the cylinder at certain angle…

Your suggested code… Im not sure if it is to effectively find the intersections (see above reasons). But also your

                if edge1 == edge2: continue

wont help too much cause if you have, say, 6 edges at that vertex, and you are processing edge1 = 3, than your if will eliminate processing of edge2 = 3, BUT it will NOT filter edge2 = 1, for example, and you may realize that the pair of edges (3-1) is equivalent to (1-3) which has been processed at edge1 = 1 and edge2 =3, I think :stuck_out_tongue:

Regards,

It looks nice, but I cannot run it though.
I’m installing it from the Add-Ons preferences window, it appears but it’s gray, not clickable.

edit: never mind, got it.

Haven’t read all posts sorry, in a rush… Dunno if it’s in your plans yet: what about removing all cylinders when I deselect the Hidden Line?

@Abidos, I’m definetely going to try your modules and scripts, cutting (and trimming and extending) are I think the modeling tools Blender is missing the most. Line segment - line segment intersections in 3d are notorious for precision issues, either use a shortest distance algorithm (e.g. http://homepage.univie.ac.at/franz.vesely/notes/hard_sticks/hst/hst.html) but better refrain from resorting to line line intersections at all. The plane - ray intersection I described above is less prone to precision errors, it will always have a solution when there ‘is’ one. You are very right about the ‘if edge1 == edge2: continue’ this should be something along the lines of ‘if edge1.index >= edge2.index: continue’. Besides that, I think the ‘cut plane intersection’ method above should work *if the joining has only to take place at vertices. Since it would eliminate 3/4 of the verts in my wire mesh, I will have another go with it later today :yes:.

@phoenixart, thanks for trying and reporting this. I don’t know what the Add-Ons preference window is, but I will find out, and fix it. For now, I am only able to make it react to the [Update] button, so when you disable Hidden Line, you still have to press update, actually that might not work, will fix that too, thanks.

The code has been updated to be compatible with the Blender Add On system:

Look under [Render] > [Hidden Line Rendering] and [Object] > [Hidden Line Rendering] to enable hidden line rendering and adjust the settings.

@Abidos:
An images says more than a thousand words: so here we go
http://www.thomaskrijnen.com/BPY/cylinder-join.jpg
Generally the method posted above works, but then again as, indeed, we are dealing with approximated cylinders it doesn’t work since the verts do not align. I am still going to investigate whether I will implement this in the hidden line render script. Also I am very interested in your approaches.

For I had in mind (“perfect welding”), I am supplying 2 pics to save two thousand words.

http://www.mediafire.com/imgbnc.php/4d5617ce2f92581f16fa3ac16fdd3fdd6g.jpg

So far it is semi-automated (see my previous post). I wish I do that possible for any mesh too (not only for octet-like ones). A matter of time to work on that again but Im now more on modeling part of Blender plus animating…

Hey Aothms,

I know the main purpose of your script is learning and having the hidden line feature, but since it looks so similar to me to this other plug in I though it could be an idea having some extra options, like other shapes for the joints. Like in the picture below:
http://www.defcon-x.de/wp-content/aaa.png

Besides that, I don’t know how much difficult would be having the buttons act in real time instead of clicking on the Update button each time. Something like we already have with the parametric values when we add a new mesh and on the Shelf we can change the values seeing the result on the 3D view port in real time. Thanks for your hard work man!

hi phoenisart, yeah I agree on discarding the update button, I still have to look into that. Regarding your other idea: back when I was still using 3dsmax I used their sweep modifier a lot, its a extrude along spline kind of thingy with custom or predefined profiles. It would be great to have something like that in Blender, but then in a mature way, also with your idea of joints and knots. That would obviously be more of a modelling tool than a rendering aid, so should definitely be another project, but I’ll give it some thought.

hi abidos, that looks sweet indeed, keep in mind that my method also provides perfect welding as long as angles between edges relate to the number of segments, e.g. 45, 30 degrees etc. For use in my rendering tool it should obviously allow any kind of arbitrary angle. Well, enjoy the modelling part of blender, I’m sure we’ll meet again sometime :wink:

The script has been updated on request to work with 2.58: http://thomaskrijnen.com/BPY/0.05/hiddenline.py

About your last version of script …
I try to use this blend file..
First , on [Object] > [Hidden Line Rendering] not working enable on secondary object.
Second , the Blender on render (F12) is crashes with error.
I don’t know if this is a script problem … maybe is too much for him…