Using non PBR raytracing
For multiple spot lamps.
You can simply rewrite a raytracer (e.g. here) which checks for rays from the surface to the point lamps. I will demonstrate the steps in a simplified way.
Get a triangulated bmesh object from the active object. The bmesh is a python module with which we can access vertices, edges and faces.
import bpy
import bmesh
def get_active_bmesh():
if bpy.context.object == None or bpy.context.object.type != 'MESH':
return None
me = bpy.context.object.data
if me.is_editmode:
bm = bmesh.from_edit_mesh(me)
else:
bm = bmesh.new()
bm.from_mesh(me)
bmesh.ops.triangulate(bm, faces=bm.faces[:], quad_method=0, ngon_method=0)
return bm
We also need to generate sample points for the faces. For that we use the triangulated faces. Luckily, bmface
s already have a builtin area calculation, if we wanted to get the relative amount of light (I will only get the absolute amount though). For the random points on the triangle we can use the method of 4.2 in this paper, mentioned in this question.
Next, we select a triangle with probability proportional to its area by generating a random number between 0 and the total cumulative area and performing a binary search on the array of cumulative areas. For each selected triangle with vertices (A, B, C), we construct a point on its surface by generating two random numbers, r1 and r2, between 0 and 1, and evaluating the following equation:
P = (1 − √r1) A + √r1(1 − r2) B + √r1r2 C
Intuitively, √r1 sets the percentage from vertex A to the opposing edge, while r2 represents the percentage along that edge (see Figure 4). Taking the square-root of r1 gives a uniform random point with
respect to surface area.
The equation translated into python
import math
import random
def get_sample_point(bmface):
assert len(bmface.verts) == 3
A, B, C = [bmface.verts[i].co for i in range(3)]
r1, r2 = [random.uniform(0, 1) for i in range(2)]
P = (1 - math.sqrt(r1)) * A + math.sqrt(r1) * (1 - r2) * B + math.sqrt(r1) * r2 * C
return P
To get the light intensity of a single sample point we need to cast a ray from the point of the sample in direction of the lamp. If the ray doesn't hit anything between the sample point and the lamp, then we can use the lamps intensity. I've implemented a simple linear falloff of a point light. This has to be adjusted for different falloff types. The following function returns the intensity at a sample point.
def get_light_intensity(origin, scn, lamps):
intensity = 0
for lamp in lamps:
lamp_co = lamp.matrix_world.to_translation()
direction = (lamp_co - origin)
result, location, normal, index, object, matrix = scn.ray_cast(origin, direction.normalized())
if not result or (location - origin).length > direction.length:
# didn't hit anything, so add the lights intensity
distance = (lamp_co - origin).length
# inverse linear falloff
intensity += max(0, lamp.data.energy-(distance/lamp.data.distance/2))
return intensity
Now we can use this function. First, we get the lamps and a bmesh object from the active object. If there was a mesh selected, we have to call faces.ensure_lookup_table()
on it to access the faces. Then, we loop through the faces and for each face
- get sample points
- calculate the light intensity at the sample points
- add the relative face intensity to the overall intensity
SAMPLE_POINT_COUNT = 50
RANDOM_SEED = 1
random.seed(RANDOM_SEED)
scn = bpy.context.scene
lamps = [ob for ob in scn.objects if ob.type == 'LAMP' and ob.data.type == 'POINT']
intensity = 0
bm = get_active_bmesh()
if bm != None:
bm.faces.ensure_lookup_table()
for bmface in bm.faces:
coords = [get_sample_point(bmface) for i in range(SAMPLE_POINT_COUNT)]
face_intensity = 0
for c in coords:
face_intensity += get_light_intensity(c, scn, lamps)
intensity = face_intensity / SAMPLE_POINT_COUNT
print(intensity)
Finally, I print the result.
Some quick tests seem to show correct results.
The todo things would be to support the different falloffs of point lights and/or add other types of lights (like sunlight). The single float output is also not the best visualization.