Scripting a reverse UV projection.
From the camera bounds shoot (raycast) an n x m resolution (simple grid uv) of rays onto the mirror object.
Estimate the normal from the hit face and the reflect vector is ray.reflect(normal)
. Project that vector onto the plane.
Debug result of shooting 16 x 9 UV onto sphere as mirror. The black arrows (empties) represent the reflection vector. The faces used to calculate the normal are selected. Notice the pole of the UVSphere is projected onto.
The resultant mesh and UV. Missing faces where a reflected ray misses plane. The pole effects the mesh on RHS.
Result of reflecting onto middle of sphere. All reflections captured UV resolution 160 x 90
Instructions
Grab the full script from below and edit to suit. Default uses a mirror object named Grid
, reflects a (160, 90)
UV map onto a plane with global location (0, -10, 0)
facing (0, 1, 0)
debug = True
# projecting from scene.camera, change for different cam
mirror_obj_name = "Grid"
# reflection plane global cordinates
plane_co = Vector((0, -10, 0))
plane_no = Vector((0, 1, 0))
# stripes The uv resolution to project from cam
XRES, YRES = (160, 90)
if debug:
# adds an empty at hit point pointing in reflect
XRES, YRES = (16, 9)
Make sure you are not in edit mode and run script. A new mesh object named "reflection" with result will be created. With debug = True
a set of empties representing location and reflect angle will be placed on mirror.
Some notes:
Make the mirror object as high resolution as possible and make sure it has vertex normals. The raycast hit normal is calculated using barycentric to map the normal of the raycast hit, from a triangle of the normals of face centre and the edge in the tessellation triangle hit.
Code:
import bpy
import bmesh
from mathutils import Vector, Matrix
from mathutils.geometry import intersect_line_plane, barycentric_transform
from math import degrees, pi, radians
debug = True
# projecting from scene.camera, change for different cam
mirror_obj_name = "Grid"
# reflection plane global cordinates
plane_co = Vector((0, -10, 0))
plane_no = Vector((0, 1, 0))
# stripes The uv resolution to project from cam
XRES, YRES = (160, 90)
if debug:
# adds an empty at hit point pointing in reflect
XRES, YRES = (16, 9)
def draw_empty_arrow(scene, name, loc, r_global, draw_size=1):
R = r_global.to_track_quat('Z', 'X').to_matrix().to_4x4()
mt = bpy.data.objects.new(name, None)
R.translation = loc
#mt.show_name = True
mt.matrix_world = R
mt.empty_draw_type = 'SINGLE_ARROW'
mt.empty_draw_size = draw_size
scene.objects.link(mt)
def calc_normal(mesh, face_index, point):
face = mesh.polygons[face_index]
# debug TODO take out
if debug:
face.select = True
o = face.center
p = point - o
# find the edge
for ek in face.edge_keys:
v0, v1 = (mesh.vertices[i] for i in ek)
if (v0.co - o).angle(p) <= (v0.co - o).angle(v1.co - o):
break
return barycentric_transform(point, o, v0.co, v1.co,
face.normal, v0.normal, v1.normal)
def obj_raycast(obj, ray_origin, ray_target, matrix=None):
# returns global coords
if matrix is None:
matrix = obj.matrix_world
# get the ray relative to the object
matrix_inv = matrix.inverted()
ray_origin_obj = matrix_inv * ray_origin
ray_target_obj = matrix_inv * ray_target
ray_direction_obj = ray_target_obj - ray_origin_obj
# cast the ray
success, location, normal, face_index = obj.ray_cast(ray_origin_obj, ray_direction_obj)
if success:
n = calc_normal(obj.data, face_index, location)
return matrix * location, (matrix * (location + n) - matrix * location).normalized(), face_index
else:
return None, None, None
context = bpy.context
scene = context.scene
render = scene.render
camera = scene.camera
# rays in global space
ray_origin = camera.location
cmw = camera.matrix_world
ar = render.resolution_y / render.resolution_x
s = Matrix.Scale(ar, 4, (0.0,1.0,0.0))
# find link to this pink, vertex code.
# get the camera frame
tr, br, bl, tl = (cmw * s * p for p in camera.data.view_frame())
# x direction stripe
origin = bl
x = br - bl
dx = x.normalized()
# y direction stripes
y = tl - bl
dy = y.normalized()
obj = scene.objects.get(mirror_obj_name) # context.obj?
matrix = obj.matrix_world
mwi = matrix.inverted()
# reflection plane
plane_co = Vector((0, -10, 0))
plane_no = Vector((0, 1, 0))
strips = []
bm = bmesh.new()
uvs = {} # uv vert lookup
#make a mesh
me = bpy.data.meshes.new("reflection")
uv_layer = bm.loops.layers.uv.verify()
bm.faces.layers.tex.verify()
for s in range(YRES + 1):
py = bl + s * y.length / float(YRES) * dy
strip = []
for b in range(XRES + 1):
p = py + b * x.length / float(XRES) * dx
ray_target = p
hit, n, f = obj_raycast(obj, ray_origin, ray_target)
if hit is not None:
# check face normal vs returned normal
face_normal = obj.data.polygons[f].normal
v = (hit - ray_origin).normalized() # incoming vector in global space
reflect = v.reflect(n) # reflect vector
v = ray_target - ray_origin
r_global = reflect
if debug:
draw_empty_arrow(scene, "UV_%d_%d" % (b, s), hit, r_global)
g = intersect_line_plane(hit,
hit + reflect,
plane_co, plane_no, False)
if reflect.angle(-plane_no) < radians(90):
vert = bm.verts.new(g)
strip.append(vert)
uvs[vert] = (b / XRES, s / YRES)
else:
strip.append(None) # miss reflect
else:
strip.append(None) # miss on hit
strips.append(strip)
# skin it
bm.verts.ensure_lookup_table()
for j in range(len(strips) - 1):
s0 = strips[j]
s1 = strips[j+1]
for i in range(len(s0) - 1):
verts = [s1[i+1], s0[i+1], s0[i], s1[i]]
if None in verts:
continue
f = bm.faces.new(verts)
# add uv
for l in f.loops:
luv = l[uv_layer]
luv.uv = uvs[l.vert]
bm.to_mesh(me)
obj = bpy.data.objects.new("reflection", me)
#obj.matrix_world = matrix
scene.objects.link(obj)
if debug:
bpy.ops.object.select_by_type(type='EMPTY')