2
$\begingroup$

I'd like to know how to make a "flat" sort of scanning beam that is cut off by various items it interacts with. For example, say that the beam emits from a single point and is shaped like a triangle as it spreads.

If, say, a cube was placed within the beam, a lack-of-beam (er, shadow) would be cast, somewhat similar to an object obscuring a spotlight beam.

If anybody was able to comprehend that, does anyone know how to make such an effect?

The attached image shows something somewhat similar to what I'm going for. enter image description here

$\endgroup$
3
  • 1
    $\begingroup$ Could you add some reference image? $\endgroup$
    – cgslav
    Commented May 23, 2018 at 18:13
  • $\begingroup$ Do you mean something like this? $\endgroup$
    – Robin Betts
    Commented May 23, 2018 at 18:32
  • $\begingroup$ @Robin Betts Sort of, except "filled in." Original post updated with somewhat accurate reference image. $\endgroup$
    – Legoman
    Commented May 24, 2018 at 15:07

3 Answers 3

11
$\begingroup$

Similiar to icYou520's answer you could create a special lamp and add a volumetric container. Since this is very render-intense here comes another hack.

enter image description here
Model by jahjavjaz

Is is a planar ngon which move vertices away from the center untill they detect a collision. This is done by a somewhat interactive python handler.

enter image description here

Execute the first code sample. The beginning holds some constants like the name of the object and the maximum distance of the rays. Increase the Radius step size will result in more vertices, and more calculation time.

import bpy
import bmesh
from mathutils.bvhtree import BVHTree
import mathutils
import math
import numpy as np

EPSILON = 0.00001
MAXIMUM_DISTANCE = 10

object_name = 'light_plane'
radius_start = math.pi*1/4
radius_end   = math.pi*3/4
radius_step  = math.pi/256

angles = [(math.cos(a), math.sin(a)) for a in np.arange(radius_start, radius_end + 0.00001, radius_step)]

def create_mesh_obj(bm, p_name = "from_bmesh"):
    scn = bpy.context.scene
    me = bpy.data.meshes.new(name = p_name)
    bm.to_mesh(me)

    if (p_name not in bpy.data.objects):
        ob = bpy.data.objects.new(name = p_name, object_data = me)
        scn.objects.link(ob)
    else:
        ob = bpy.data.objects[p_name]
        ob.data = me
    scn.update()
    return ob

def create_vertice_shapes(angles):
    bm = bmesh.new()
    for a in angles:
        bm.verts.new((a[0], a[1], 0))
    bm.verts.new((0, 0, 0))
    ob = create_mesh_obj(bm)
    bm.free()
#    basis = ob.shape_key_add(from_mix=False)
#    basis.name = "basis"
    return ob

ob = create_vertice_shapes(angles)
ob.name = object_name

The script give us an arc of vertices and a center vertices. Make a face. F

enter image description here

The second snippet contains the method to detect collision using the bvhtree from the mathutils module. Make sure, the objects name is correct.

Uncommenting one the last three lines will give us a certain result.

  1. bpy.app.handlers.scene_update_pre.append(my_handler)
    This line registers the function as a handler on scene update. After uncommenting this line and running the script you will see a live update of the calculation. Depending on your scene this will be computationally expensive.
  2. bpy.app.handlers.frame_change_pre.append(my_handler)
    This line registers the function as a handler on framechange. Useful, if you only want to see the update on playback to view the animation, or if you want to render the animation.
  3. my_handler(bpy.context.scene)
    This line executes the function immediately, and only once.
import bpy
import bmesh
from mathutils.bvhtree import BVHTree
import mathutils
import math
import numpy as np

EPSILON = 0.00001
MAXIMUM_DISTANCE = 5

object_name = 'light_plane'
radius_start = math.pi*1/4
radius_end   = math.pi*3/4
radius_step  = math.pi/256


angles = [(math.cos(a), math.sin(a)) for a in np.arange(radius_start, radius_end + 0.00001, radius_step)]
print(angles)

def set_positions(ob, angles, obstacles):
    trees = []
    for obstacle in obstacles:
        bm = bmesh.new()
        bm.from_object(obstacle, bpy.context.scene)
        bmesh.ops.transform(bm, matrix=obstacle.matrix_world, verts=bm.verts)
        trees.append(BVHTree.FromBMesh(bm, epsilon=EPSILON))

    for idx, a in enumerate(angles):
        min_dist = MAXIMUM_DISTANCE
        v_loc = mathutils.Vector((a[0], a[1], 0)) * MAXIMUM_DISTANCE
        for bvh in trees:
            v = (ob.matrix_world * mathutils.Vector((a[0], a[1], 0))) - ob.location
            loc, no, i, d = bvh.ray_cast(ob.location, v)
            if d is not None:
                if d < min_dist:
                    min_dist = d
                    v_loc = ob.matrix_world.inverted() * loc
        ob.data.vertices[idx].co = v_loc


def my_handler(scene):
    ob = bpy.data.objects[object_name]
    selection = [oo for oo in bpy.data.objects if oo != ob]
    print(ob.location)
    set_positions(ob, angles, selection)

for h in bpy.app.handlers.scene_update_pre:
    bpy.app.handlers.scene_update_pre.remove(h)
for h in bpy.app.handlers.frame_change_pre:
    bpy.app.handlers.frame_change_pre.remove(h)
#bpy.app.handlers.scene_update_pre.append(my_handler)
#bpy.app.handlers.frame_change_pre.append(my_handler)
#my_handler(bpy.context.scene)
$\endgroup$
4
  • $\begingroup$ Unfortunately, I'm terrible with code. I ran the first script and it gave me the arc. I made the face, then I ran the second script...and nothing happened. $\endgroup$
    – Legoman
    Commented May 28, 2018 at 17:00
  • 1
    $\begingroup$ @Legoman Did you remember to uncomment the second last line by removing the # symbol. Did you check that the created object was correctly named "light_plane"? Did you look in the console for error messages? $\endgroup$
    – Leander
    Commented May 28, 2018 at 17:02
  • $\begingroup$ Yes, tried all of those. Does running the script multiple times (trying each of the uncommenting options) mess things up? $\endgroup$
    – Legoman
    Commented May 28, 2018 at 17:58
  • 1
    $\begingroup$ If you have trouble reproducing the steps you can open up a new question particularising this specific problem. $\endgroup$
    – Leander
    Commented Jun 6, 2018 at 8:09
6
$\begingroup$

Updated version of @Leanders answer for 2.8

enter image description here

Re a question re updating @Leander's script TypeError: expected 'Depsgraph' type found 'Scene' instead thought I would have a tilt at giving it a 2.8 slant.

The theory is much the same, re creating the object then moving verts with a handler.

Creating the object

  • Put the object in a collection named "LightRays" and link it to the current scene
  • Add a custom property for the maximum ray length.

Script

import bpy
from bpy import context
import bmesh
from math import radians


angle = radians(45)
steps = 256
max_ray_length = 10.0


bm = bmesh.new()
v0 = bm.verts.new()
v1 = bm.verts.new((max_ray_length, 0, 0))

bmesh.ops.spin(
    bm,
    angle=angle,
    steps=steps,
    geom=[v1],
    axis=(0, 0, 1),
    )
    
bmesh.ops.contextual_create(
    bm,
    geom=bm.verts,
    )

scene = context.scene   
coll = bpy.data.collections.get("LightRays")

if not coll:     
    coll = bpy.data.collections.new("LightRays")
if coll.name not in scene.collection.children:
    scene.collection.children.link(coll)


me = bpy.data.meshes.new("LightRay")
bm.to_mesh(me)
ob = bpy.data.objects.new("LightRay", me)
ob["max_ray_length"] = max_ray_length 
coll.objects.link(ob)

The handler.

  • Look at all objects in the light ray collection
  • Hide them all to make them invisible to raycast.
  • Use scene raycast from 0th vert in direction of each other vert into the scene. If it hits an object set that as loc, otherwise set to max length.
  • Note Have simply used the vert coord for direction, this could go horribly wrong if an object hits at origin of light (creating zero (directionless) vectors)
  • Unhide the ray objects.
  • Have used the frame change post handler. This allows animating of the max_ray_length custom property. The lightray objects can be edited.

Script.

import bpy
from mathutils import Vector

def ray(view_layer, scene, ev_ob, me):
    ob = ev_ob
    max_ray_length = ob.get("max_ray_length", 1)
    mw = ob.matrix_world.copy()
    dz = mw @ Vector((0, 0, 1e-5))
    mwi = mw.inverted()
    for v in me.vertices[1:]:
        d = mw @ v.co - mw.translation
        hit, loc, _, _, hit_ob, _ = scene.ray_cast(
            view_layer,
            mw.translation,
            d.normalized(),
            distance = max_ray_length,
            )
            
        v.co = mwi @ (loc) if hit_ob else max_ray_length * v.co.normalized()
            
        
def light_ray(scene, depsgraph):
    coll = scene.collection.children.get("LightRays")
    if coll:
        view_layer = scene.view_layers[0] # active?:
        for ob in coll.objects:
            ob.hide_set(True, view_layer=view_layer)
        for ob in coll.objects:
            if not ob.type == 'MESH':
                continue
            ray(view_layer, scene, ob.evaluated_get(depsgraph), ob.data)
        for ob in coll.objects:
            ob.hide_set(False, view_layer=view_layer)
       
bpy.app.handlers.frame_change_post.clear()
bpy.app.handlers.frame_change_post.append(light_ray)

Render test.

Quick test render of animation above.

enter image description here

Workbench render of two 120 degree 512 vert lightray objects. Both have a simple frame / 24 driver to rotate on z based on the scene frame. The red is parented to a tilted empty.

enter image description here

$\endgroup$
3
$\begingroup$

I came up with a hacky way to do it. (I am also working on another solution that would work much better but running into problems with that one. I will update this answer with a 2nd option when I figure it out).

I just created a plane scaled on Y to make it thin and subdivided about 6 times.

I created a very small and sharp spot light.

Put a hair particle system on the thin plane and chose the spot light as the object. (More details on how to do this ).

Make sure your particles system is set to faces and even distribution. Then just put as many particles (lights) until it makes a straight line. I used about 350.

Then I made another plane for the "filled in part". The node set up is just an emission shader with some noise and wave node mixed in.

This way creates LOTS of noise and computer gets slow. So may need very high samples and will have to clamp indirect and possibly direct lights.

enter image description here

enter image description here

enter image description here

enter image description here

$\endgroup$
1
  • $\begingroup$ Yeeks, that's not the most practical method....looking forward to your second method! $\endgroup$
    – Legoman
    Commented May 25, 2018 at 20:30

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .