Lighting 2D triangles from a 3D mesh

calendar_today

01.05.2022

label

VEX, Python, Rendering

mouse

Houdini 19.0

1 Introduction

Let's display a triangulated 3D mesh as a lit and shaded perspective view in the form of 2D triangles. This supports the cameras perspective including removal of backface polygons as well as shading and lighting with attenuation and raytraced shadows.

2 Process

First we integrate the parameters for the camera and light operator path along with an intensity slider. Based on this we'll extract the camera perspective and light directions which are then being used for shading/lighting as well as raycasting each point towards the camera and the light. We'll remove the points if any of their rays intersect with the mesh indicating they are either not seen and/or not lit. Lastly, we transform the point positions into camera space and, based on our lighting calculation, output an inset attribute to be used later in a poly extrude node.

3 Code

// PARAMETERS
string cam = chs('camera');
string light = chs('light');
float intens = chf('intensity');

// PERSPECTIVE
vector pos_near = fromNDC(cam, {0.5, 0.5, 0.0});
vector pos_far = fromNDC(cam, {0.5, 0.5, -0.1});
vector dir_cam = normalize(pos_near - pos_far);
vector pos = toNDC(cam, v@P);
pos.y *= 9.0/16.0;

// LIGHTING
matrix xform_light = optransform(light);
vector pos_light = cracktransform(0, 0, 0, 0, 0, xform_light);
vector dir_light = normalize(pos_light - v@P);

// SHADING
vector nml = normalize(v@N);
float atten = intens - distance2(v@P, pos_light);
float lighting = dot(nml, dir_light);
float shading = max(dot(nml, dir_cam), 0.0);
float inset = min(atten, lighting, shading);

// RAYCASTING
vector pos_srf = v@P + nml * 1e-3;
int prim_cam = intersect(0, pos_srf, dir_cam, set(0), set(0));
int prim_light = intersect(0, pos_srf, dir_light, set(0), set(0));
if(prim_cam + prim_light >= 0) removepoint(0, i@ptnum, 0);

// ATTRIBUTES
v@P = set(pos.x, pos.y, 0.0);
f@inset = fit01(inset, 0.0025, 0.0);

4 Export to SVG

Scale the triangles, eg. by 600 in advance and set maxsize accordingly for an SVG image with 600 pixels.

node = hou.pwd()
geo = node.geometry()

filename = node.evalParm('filename')
maxsize = node.evalParm('maxsize')

def write_path(fp, points):
  if not points:
    return
  data = 'M{:.1f} {:.1f} '.format(points[0].x(), points[0].y())
  for p in points[1:]:
    data += 'L{:.1f} {:.1f} '.format(p.x(), p.y())
  fp.write('<path d="{}Z" class="s" />\n'.format(data))

with open(filename, 'w') as fp:
  fp.write('<?xml version="1.0" standalone="no"?>\n')
  fp.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
  fp.write('<svg width="{}px" height="{}px" version="1.1" xmlns="http://www.w3.org/2000/svg">\n'.format(maxsize, maxsize))
  fp.write('<defs>\n')
  fp.write('<style type="text/css"><![CDATA[\n')
  fp.write('.s { stroke: black; stroke-width: 0.8px; fill: transparent; }\n')
  fp.write('.s:hover { stroke-width: 0.0px; fill: red; }\n')
  fp.write(']]></style>\n')
  fp.write('</defs>\n')
  for prim in geo.iterPrims():
    if prim.type() != hou.primType.Polygon:
      continue
    points = [v.point().position() for v in prim.vertices()]
    write_path(fp, points)
  fp.write('</svg>')

5 Model source

download

Downloads