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