intensity color with custom ray tracing
Reference the full script on github or at the end of the answer.




Note, that this will be the wrong approach in most cases. Your own bpy raytracer will lack features and be slow and inefficient. There are other APIs (nvidia link>) which are probably more suited for this purpose.

When testing, use small resolutions and few lamps.



To calculate the colors of an image, for each pixel, we have to: 


Shoot a ray and check if it intersects. If it doesn't, this pixel stays at an alpha of 0.
If a ray hit an object, get the hit position.
Send a ray to each lamp in from the hit position. If this new ray hits another object, the lamp is obstructed and will not contribute lighting to this pixel. Otherwise calculate the light contribution (with the lamps color, intensity, falloff and object normal) and add it to the pixel.


I'm going to use numpy to store the pixel values and copy them onto an image at the end. This is faster than manipulating a reference to a blender image.

import bpy
import numpy as np
import mathutils


I will store a reference to the scene, the lamps, and the render settings. The width and height of the image can by directly deduced from the render settings. I will use the sensor_width as a fixed constant and calculate the sensor_height relative to the images dimensions. For 2.8, we have to access the view_layer when using the ray_cast function of the scene.

scn = bpy.context.scene
view_layer = bpy.context.view_layer
cam_mat = scn.camera.matrix_world
cam_loc, _, _ = cam_mat.decompose()
lamps = [ob for ob in bpy.data.objects if ob.type=='LAMP']
cam = scn.camera.data
render = scn.render
horizontal = cam.sensor_width/2
vertical = horizontal/render.resolution_x*render.resolution_y

width = render.resolution_x
height = render.resolution_y


Construct the base image array and the corresponding rays. Read this answer and this post on how blender stores images.

For now we will create a pixel list which looks like this:

[y coordinate][x coordinate][rgba index]


and can by done with numpy

pixels = np.zeros((height, width, 4), dtype=float)


The rays are a grid starting from (0, 0, 0) and pointing downwards. The focal length equals their negative Z coordinate and their x, y range is stored in the sensor settings.

ray_width = np.linspace(-horizontal, horizontal, num=width, dtype=float).reshape(1, -1)
ray_width = np.repeat(ray_width, height, axis = 0)
ray_height = np.linspace(-vertical, vertical, num=height, dtype=float).reshape(-1, 1)
ray_height = np.repeat(ray_height, width, axis = 1)
ray_depth = np.zeros((height, width), dtype=float) - cam.lens

rays = np.stack([ray_width, ray_height, ray_depth], axis = 2)


Each ray will have to be transformed by the camera's transformation matrix and be shot from camera_position in direction of camera_position to transformed_ray.

Now we iterate through all the pixels in the image by x, y coordinates. We get the ray for each pixel and transform it by the camera's matrix_world. If the ray cast hit something, that means this pixel should be colored and visible.

for y in range(height):
    for x in range(width):
        ray = cam_mat @ mathutils.Vector(rays[y, x]) - cam_loc
        result, loc, nor, ind, ob, mat = scn.ray_cast(view_layer, cam_loc, ray)

        if (result):
            intensity = base_intensity[:]
            pixels[y, x] = intensity[0], intensity[1], intensity[2], 255


To make lights influence the pixel's values, check if there is a clear path from the last ray_casts position loc. If yes add this lights color and to the intensity. I used a linear falloff like 1 - (point_light_distance / lamp_light_distance). Multiply this by the result of the dotproduct of the surface normal and the direction to the light. This will stop making only faces which directly face the light receive the full amount of light.

        if (result):
            intensity = base_intensity[:]

            for lamp in lamps:
                dir = lamp.location - loc
                dirn = dir.normalized()

                start = loc + dirn * 1e-4
                hit,_,_,_,_,_ = scn.ray_cast(view_layer, start, dirn)
                if not hit:
                    multiplier = max(0, min(1, 1 - dir.length / lamp.data.distance)) * lamp.data.energy * max(0, dirn.dot(nor))
                    intensity[0] += multiplier * lamp.data.color[0]
                    intensity[1] += multiplier * lamp.data.color[1]
                    intensity[2] += multiplier * lamp.data.color[2]

            pixels[y, x] = intensity[0], intensity[1], intensity[2], 255


Finally, create a new image (or use an existing one) and replace the images pixels list with our flattened pixels array.

img = bpy.data.images.get("name")
if (    (not img) or
        (img.size[0] != width or img.size[1] != height)):
    img = bpy.data.images.new("name", width, height)
img.pixels = pixels.reshape(-1)




Full script 

import bpy
import numpy as np
import mathutils

scn = bpy.context.scene
base_intensity = list(scn.world.color)
view_layer = bpy.context.view_layer
cam_mat = scn.camera.matrix_world
cam_loc, _, _ = cam_mat.decompose()
lamps = [ob for ob in scn.objects if ob.type=='LIGHT']
cam = scn.camera.data
render = scn.render
horizontal = cam.sensor_width/2
vertical = horizontal/render.resolution_x*render.resolution_y

width = render.resolution_x
height = render.resolution_y
rays = np.zeros((height, width, 3), dtype=float)

ray_width = np.linspace(-horizontal, horizontal, num=width, dtype=float).reshape(1, -1)
ray_width = np.repeat(ray_width, height, axis = 0)
ray_height = np.linspace(-vertical, vertical, num=height, dtype=float).reshape(-1, 1)
ray_height = np.repeat(ray_height, width, axis = 1)
ray_depth = np.zeros((height, width), dtype=float) - cam.lens

rays = np.stack([ray_width, ray_height, ray_depth], axis = 2)

pixels = np.zeros((height, width, 4), dtype=float)

for y in range(height):
    for x in range(width):
        ray = cam_mat @ mathutils.Vector(rays[y, x]) - cam_loc
        result, loc, nor, ind, ob, mat = scn.ray_cast(view_layer, cam_loc, ray)

        if (result):
            intensity = base_intensity[:]

            for lamp in lamps:
                dir = lamp.location - loc
                dirn = dir.normalized()

                start = loc + dirn * 1e-4
                hit,_,_,_,_,_ = scn.ray_cast(view_layer, start, dirn)
                if not hit:
                    multiplier = max(0, min(1, 1 - dir.length / lamp.data.distance)) * lamp.data.energy * max(0, dirn.dot(nor))
                    intensity[0] += multiplier * lamp.data.color[0]
                    intensity[1] += multiplier * lamp.data.color[1]
                    intensity[2] += multiplier * lamp.data.color[2]

            pixels[y, x] = intensity[0], intensity[1], intensity[2], 255

img = bpy.data.images.get("name")
if (    (not img) or
        (img.size[0] != width or img.size[1] != height)):
    img = bpy.data.images.new("name", width, height)
img.pixels = pixels.reshape(-1)