Rendering 3D vector curves to 2D vector curves (polylines) with visibility in Blender
Setup: you have a BezierCurve in blender, and you would like to render it to svg file.
SVG file:Â winding.svg
You can render curves with Blender Add-on with Bézier Utility Operations (https://github.com/Shriinivas/blenderbezierutils) but it does not take into account curve visibility (curve is still there even though it is occluded by an object in the scene). See the inset here.
SVG file:Â winding_wrong.svg
TLDR: sample points on the curve and check if this point is visible from camera or it is occluded by an object in your scene – see ray_cast
 function. Then compose polylines from visible points.
Limitation: the result will be a collection of polylines, not bezier curves. It seems possible to do it properly by projecting bezier curve on screen space and splitting it into visible and invisible parts using same logic as here.
Warning: this is a demo script that only takes one curve. You might want to update it to render multiple curves to one SVG file
import bpy
import bpy_extras
import svgwrite
curve = bpy.data.objects.get("myCurve.003") # name of curve to render. make this script a loop if you want to iterate over many curves
camera = bpy.data.objects.get("Camera")
# Get the active scene
scene = bpy.context.scene
# Get the dependency graph for the scene
depsgraph = bpy.context.evaluated_depsgraph_get()
list_of_lines = []
current_line = []
# here we form a list of 3d points to check for visibility and build polylines
# assuming that Bezier curve is densely subdivided, we only look a the vertices
points_to_check = list(enumerate(curve.data.splines[0].bezier_points))
# if curve data contains multiple splines, iterate over curve.data.splines to form points_to_check
# when cyclic_u we need to double-check the starting point
if curve.data.splines[0].use_cyclic_u:
points_to_check.append(points_to_check[0])
for (i,bp) in points_to_check:
# https://docs.blender.org/api/current/bpy.types.Scene.html#bpy.types.Scene.ray_cast
point = bp.co
direction = (point - camera.location).normalized()
# print(f"\n===={i}")
# print(f"Point: {point}")
# print("Direction:", direction)
is_intersecting_solids, location, normal, index, object, matrix = bpy.context.scene.ray_cast(
depsgraph,
camera.location,
direction,
)
# ray_cast will not compute intersection with Bezier curve, so is_intersecting_solids is True when ray falls on some solid object in your scene
# let's check if the interection happens after we see this point on a curve
# if is_intersecting_solids is False we don't need to compute it
is_intersecting_after = False
if is_intersecting_solids:
distance_curve = (point - camera.location).length
distance_object = (location - camera.location).length
# print(f"distance curve: {distance_curve}")
# print(f"distance object: {distance_object}")
is_intersecting_after = distance_object > distance_curve
point_is_visible = (not is_intersecting_solids) or is_intersecting_after
    # point is not occluded by a shape. now we find its pixel coordinates and don't plot it if it is outside the frame
point_is_in_frame = False
if point_is_visible:
# check that it is in frame using normalized device coordinates (NDC)
# https://docs.blender.org/api/current/bpy_extras.object_utils.html#bpy_extras.object_utils.world_to_camera_view
ndc_coords = bpy_extras.object_utils.world_to_camera_view(
scene, camera, point
)
# print(ndc_coords)
if (0<= ndc_coords.x <= 1) and (0<= ndc_coords.y <= 1) and (ndc_coords.z >= 0):
point_is_in_frame = True
# calculate pixel coordinates in camera
render_scale = scene.render.resolution_percentage / 100
point2d = [ndc_coords.x * scene.render.resolution_x * render_scale, (1 - ndc_coords.y) * scene.render.resolution_y * render_scale]
current_line.append(point2d)
# print("point 2d:", point2d)
if point_is_in_frame:
continue
else:
if len(current_line) > 1:
list_of_lines.append(current_line)
current_line = []
# break
if len(current_line) > 1:
list_of_lines.append(current_line)
current_line = []
print(f"\n---> found {len(list_of_lines)} lines")
#print(list_of_lines)
# utility function to output svg file
def write_lines_tosvg(
lines_list,
filename="lines2.svg",
width=scene.render.resolution_x,
height=scene.render.resolution_y,
):
dwg = svgwrite.Drawing(filename, profile='tiny')
dwg['width'] = '{}px'.format(width)
dwg['height'] = '{}px'.format(height)
for line in lines_list:
polyline = dwg.polyline(line, stroke='black', fill='none')
dwg.add(polyline)
dwg.save()
write_lines_tosvg(list_of_lines)
In macos:Â
./Applications/Blender.app/Contents/Resources/4.0/python/bin/python3.10 -m pip install svgwrite