435 lines
13 KiB
Python
435 lines
13 KiB
Python
import bpy
|
|
import bmesh
|
|
from mathutils import Vector, Euler
|
|
from collections import defaultdict
|
|
from dataclasses import dataclass
|
|
import colorsys
|
|
|
|
def sprint(*text):
|
|
screen = bpy.data.screens['Scripting']
|
|
for area in screen.areas:
|
|
if area.type != "CONSOLE":
|
|
continue
|
|
override = {'screen': screen, 'area': area}
|
|
with bpy.context.temp_override(**override):
|
|
bpy.ops.console.scrollback_append(text=" ".join(map(str, text)))
|
|
|
|
print = sprint
|
|
|
|
@dataclass
|
|
class Edge:
|
|
a: int # vertex index
|
|
b: int # vertex index
|
|
|
|
@dataclass
|
|
class PolygonIndex:
|
|
a: int # polygon index
|
|
b: int # polygon index
|
|
|
|
@dataclass
|
|
class EdgePolygon:
|
|
edge: Edge
|
|
polygon_index: PolygonIndex
|
|
|
|
@dataclass
|
|
class Graph:
|
|
a: int # index into edge_indices
|
|
b: int # index into edge_indices
|
|
|
|
def select_edge(bm, edge):
|
|
for e in bm.edges:
|
|
if frozenset((e.verts[0].index, e.verts[1].index)) == frozenset((edge.a, edge.b)):
|
|
e.select = True
|
|
|
|
def select_edge_single(edge):
|
|
obj = bpy.context.edit_object
|
|
bm = bmesh.from_edit_mesh(obj.data)
|
|
select_edge(bm, edge)
|
|
bmesh.update_edit_mesh(obj.data)
|
|
|
|
def select_light_faces(indicators):
|
|
obj = bpy.context.edit_object
|
|
bm = bmesh.from_edit_mesh(obj.data)
|
|
for i, v in enumerate(indicators):
|
|
if v > 0:
|
|
bm.faces[i].select = True
|
|
else:
|
|
bm.faces[i].select = False
|
|
|
|
bmesh.update_edit_mesh(obj.data)
|
|
|
|
def select_silhouette(edge_polygons: list[EdgePolygon],
|
|
edge_indices: list[int],
|
|
edge_indices_length: int):
|
|
obj = bpy.context.edit_object
|
|
bm = bmesh.from_edit_mesh(obj.data)
|
|
|
|
for i in range(len(edge_polygons)):
|
|
bm.edges[i].select = False
|
|
|
|
for i in range(edge_indices_length):
|
|
ep = edge_polygons[edge_indices[i]]
|
|
select_edge(bm, ep.edge)
|
|
|
|
bmesh.update_edit_mesh(obj.data)
|
|
|
|
def face_indicators(light: Vector,
|
|
position: list[Vector],
|
|
polygon_normal: list[Vector],
|
|
mesh: bpy.types.Mesh,
|
|
# outputs
|
|
indicators: list[float]):
|
|
for i in range(len(mesh.polygons)):
|
|
n = polygon_normal[i]
|
|
p = position[mesh.polygons[i].vertices[0]]
|
|
indicator = n.dot(light - p)
|
|
indicators[i] = indicator
|
|
|
|
def build_edge_polygons(mesh: bpy.types.Mesh):
|
|
by_edge = defaultdict(list)
|
|
for i, polygon in enumerate(mesh.polygons):
|
|
for edge in polygon.edge_keys:
|
|
by_edge[frozenset(edge)].append(i)
|
|
|
|
#l = [(edge, p) for edge, p in by_edge.items() if len(p) != 2]
|
|
#print(l[0])
|
|
#edge, _ = l[0]
|
|
#select_edge_single(Edge(*edge))
|
|
|
|
assert all(len(p) == 2 for p in by_edge.values())
|
|
|
|
return [
|
|
EdgePolygon(Edge(*edge), PolygonIndex(*polygons))
|
|
for edge, polygons in by_edge.items()
|
|
]
|
|
|
|
def object_silhouette(indicators: list[float],
|
|
edge_polygons: list[EdgePolygon],
|
|
# outputs
|
|
edge_indices: list[int]):
|
|
ix = 0
|
|
|
|
for i in range(len(edge_polygons)):
|
|
ep = edge_polygons[i]
|
|
if (indicators[ep.polygon_index.a] > 0) != (indicators[ep.polygon_index.b] > 0):
|
|
edge_indices[ix] = i
|
|
ix += 1
|
|
|
|
return ix
|
|
|
|
def graph_append(g, v):
|
|
assert g.a == -1 or g.b == -1
|
|
if g.a == -1:
|
|
g.a = v
|
|
else:
|
|
g.b = v
|
|
|
|
def edge_loop_graph(mesh: bpy.types.Mesh,
|
|
edge_polygons: list[EdgePolygon],
|
|
edge_indices: list[int],
|
|
edge_indices_length: list[int],
|
|
#output
|
|
graph: list[Graph]):
|
|
for i in range(len(mesh.vertices)):
|
|
graph[i].a = -1
|
|
graph[i].b = -1
|
|
|
|
for i in range(edge_indices_length):
|
|
edge_index = edge_indices[i]
|
|
edge = edge_polygons[edge_index].edge
|
|
graph_append(graph[edge.a], i)
|
|
graph_append(graph[edge.b], i)
|
|
|
|
def graph_next_neighbor(g: Graph,
|
|
ix: int):
|
|
if g.a == ix:
|
|
return g.b
|
|
else:
|
|
return g.a
|
|
|
|
def edge_loop_inner(edge_polygons: list[EdgePolygon],
|
|
edge_indices: list[int],
|
|
graph: list[Graph],
|
|
# output
|
|
visited_edge_indices: list[bool],
|
|
ix: int,
|
|
edge_loop: list[int],
|
|
edge_loop_ix: int):
|
|
e = edge_polygons[edge_indices[ix]].edge
|
|
edge_loop[edge_loop_ix] = e.b
|
|
edge_loop_ix += 1
|
|
|
|
while True:
|
|
visited_edge_indices[ix] = True
|
|
e = edge_polygons[edge_indices[ix]].edge
|
|
next_ix_a = graph_next_neighbor(graph[e.a], ix)
|
|
next_ix_b = graph_next_neighbor(graph[e.b], ix)
|
|
|
|
if visited_edge_indices[next_ix_a] == False:
|
|
edge_loop[edge_loop_ix] = e.a
|
|
edge_loop_ix += 1
|
|
ix = next_ix_a
|
|
elif visited_edge_indices[next_ix_b] == False:
|
|
edge_loop[edge_loop_ix] = e.b
|
|
edge_loop_ix += 1
|
|
ix = next_ix_b
|
|
else:
|
|
break
|
|
|
|
return edge_loop_ix
|
|
|
|
def next_unvisited(visited_edge_indices: list[bool],
|
|
edge_indices_length: int):
|
|
for i in range(edge_indices_length):
|
|
if visited_edge_indices[i] == False:
|
|
return i
|
|
return -1
|
|
|
|
def edge_loop(edge_polygons: list[EdgePolygon],
|
|
edge_indices: list[int],
|
|
edge_indices_length: int,
|
|
graph: list[Graph],
|
|
edge_loops: list[int],
|
|
edge_loop_lengths: list[int],
|
|
max_edge_loops: int):
|
|
visited_edge_indices = [False] * edge_indices_length
|
|
|
|
edge_loop_ix = 0
|
|
i = 0
|
|
#while i < max_edge_loops:
|
|
while True:
|
|
start = next_unvisited(visited_edge_indices, edge_indices_length)
|
|
if start == -1:
|
|
break
|
|
assert i < max_edge_loops
|
|
|
|
new_edge_loop_ix = edge_loop_inner(edge_polygons,
|
|
edge_indices,
|
|
graph,
|
|
visited_edge_indices,
|
|
start,
|
|
edge_loops,
|
|
edge_loop_ix)
|
|
length = new_edge_loop_ix - edge_loop_ix
|
|
edge_loop_lengths[i] = length
|
|
edge_loop_ix = new_edge_loop_ix
|
|
|
|
i += 1
|
|
|
|
return i
|
|
|
|
def list_init(l, init):
|
|
for i in range(len(l)):
|
|
l[i] = init(0, 0)
|
|
|
|
def print_edge_loops(edge_loops: list[int],
|
|
edge_loop_lengths: list[int],
|
|
edge_loop_count: int):
|
|
edge_loop_ix = 0
|
|
for i in range(edge_loop_count):
|
|
length = edge_loop_lengths[i]
|
|
l = [edge_loops[edge_loop_ix + j] for j in range(length)]
|
|
s = ", ".join(map(str, l))
|
|
print(f"loop {i} [{s}]")
|
|
edge_loop_ix += length
|
|
|
|
def reset_mesh_colors(mesh):
|
|
for color in mesh.color_attributes['Attribute'].data:
|
|
color.color = (1, 1, 1, 1)
|
|
|
|
def color_edge_loop(mesh,
|
|
edge_loops: list[int],
|
|
edge_loop_lengths: int,
|
|
edge_loop_count: int):
|
|
edge_loop_ix = 0
|
|
for i in range(edge_loop_count):
|
|
length = edge_loop_lengths[i]
|
|
|
|
for j in range(length):
|
|
fraction = j / length
|
|
r, g, b = colorsys.hsv_to_rgb(fraction, 1, j != 0)
|
|
|
|
vertex_index = edge_loops[edge_loop_ix + j]
|
|
for polygon in mesh.polygons:
|
|
if vertex_index in polygon.vertices:
|
|
ix = list(polygon.vertices).index(vertex_index)
|
|
loop_ix = polygon.loop_indices[ix]
|
|
mesh.color_attributes['Attribute'].data[loop_ix].color_srgb = (r, g, b, 1)
|
|
|
|
edge_loop_ix += length
|
|
|
|
def cast_ray(light: Vector,
|
|
start: Vector):
|
|
ray = start - light
|
|
ray.normalize()
|
|
return start + (ray * 7.0)
|
|
|
|
def link_bmesh(bm, name):
|
|
mesh = bpy.data.meshes.new(name)
|
|
bm.to_mesh(mesh)
|
|
bm.free()
|
|
object = bpy.data.objects.new(name, mesh)
|
|
bpy.context.scene.collection.objects.link(object)
|
|
|
|
def shadow_volume_mesh_rays(bm,
|
|
bm_position: list[bmesh.types.BMVert],
|
|
bm_cast_position: list[bmesh.types.BMVert],
|
|
edge_loops: list[int],
|
|
edge_loop_ix: int,
|
|
edge_loop_length: int):
|
|
for i in range(edge_loop_length):
|
|
j = (i + 1) % edge_loop_length
|
|
|
|
i1 = edge_loops[edge_loop_ix + i]
|
|
i2 = edge_loops[edge_loop_ix + j]
|
|
|
|
a = bm_position[i1]
|
|
b = bm_position[i2]
|
|
c = bm_cast_position[i2]
|
|
d = bm_cast_position[i1]
|
|
|
|
bm.faces.new([a, b, c, d])
|
|
|
|
def shadow_volume_end_caps(bm,
|
|
bm_position: list[bmesh.types.BMVert],
|
|
bm_cast_position: list[bmesh.types.BMVert],
|
|
mesh,
|
|
indicators: list[float]):
|
|
for i in range(len(mesh.polygons)):
|
|
p = mesh.polygons[i]
|
|
if indicators[i] > 0:
|
|
a = bm_position[p.vertices[0]]
|
|
b = bm_position[p.vertices[1]]
|
|
c = bm_position[p.vertices[2]]
|
|
d = bm_position[p.vertices[3]]
|
|
bm.faces.new([a, b, c, d])
|
|
else:
|
|
a = bm_cast_position[p.vertices[0]]
|
|
b = bm_cast_position[p.vertices[1]]
|
|
c = bm_cast_position[p.vertices[2]]
|
|
d = bm_cast_position[p.vertices[3]]
|
|
bm.faces.new([a, b, c, d])
|
|
|
|
def main():
|
|
light = bpy.data.objects['Light'].location
|
|
torus = bpy.data.objects['Torus']
|
|
|
|
position = [0] * len(torus.data.vertices)
|
|
for i, vertex in enumerate(torus.data.vertices):
|
|
position[i] = torus.matrix_world @ vertex.co
|
|
|
|
polygon_normal = [0] * len(torus.data.polygon_normals)
|
|
for i, normal in enumerate(torus.data.polygon_normals):
|
|
n = torus.matrix_world.to_3x3() @ normal.vector
|
|
n.normalize()
|
|
polygon_normal[i] = n
|
|
|
|
mesh = torus.data
|
|
|
|
indicators = [0] * len(torus.data.polygon_normals)
|
|
|
|
face_indicators(light,
|
|
position,
|
|
polygon_normal,
|
|
mesh,
|
|
# outputs
|
|
indicators)
|
|
|
|
# select_light_faces(indicators)
|
|
|
|
edge_polygons = build_edge_polygons(mesh)
|
|
|
|
edge_indices = [0] * len(edge_polygons)
|
|
|
|
edge_indices_length = object_silhouette(indicators,
|
|
edge_polygons,
|
|
# outputs
|
|
edge_indices)
|
|
|
|
# select_silhouette(edge_polygons, edge_indices, edge_indices_length)
|
|
|
|
# graph is a mapping from vertex indices to edge_indices
|
|
graph = [0] * len(mesh.vertices)
|
|
list_init(graph, Graph)
|
|
edge_loop_graph(mesh,
|
|
edge_polygons,
|
|
edge_indices,
|
|
edge_indices_length,
|
|
# outputs
|
|
graph)
|
|
|
|
#print(graph)
|
|
|
|
max_edge_loops = 2
|
|
edge_loops = [0] * edge_indices_length
|
|
edge_loop_lengths = [0] * max_edge_loops
|
|
edge_loop_count = edge_loop(edge_polygons,
|
|
edge_indices,
|
|
edge_indices_length,
|
|
graph,
|
|
edge_loops,
|
|
edge_loop_lengths,
|
|
max_edge_loops)
|
|
print("edge loop count", edge_loop_count)
|
|
|
|
print_edge_loops(edge_loops, edge_loop_lengths, edge_loop_count)
|
|
|
|
"""
|
|
reset_mesh_colors(mesh)
|
|
color_edge_loop(mesh,
|
|
edge_loops,
|
|
edge_loop_lengths,
|
|
edge_loop_count)
|
|
"""
|
|
|
|
cast_position = [0] * len(mesh.vertices)
|
|
for i in range(len(mesh.vertices)):
|
|
cast_position[i] = cast_ray(light, position[i])
|
|
|
|
bm = bmesh.new()
|
|
bm_position = list(map(bm.verts.new, position))
|
|
bm_cast_position = list(map(bm.verts.new, cast_position))
|
|
|
|
edge_loop_ix = 0
|
|
for i in range(edge_loop_count):
|
|
edge_loop_length = edge_loop_lengths[i]
|
|
shadow_volume_mesh_rays(bm,
|
|
bm_position,
|
|
bm_cast_position,
|
|
edge_loops,
|
|
edge_loop_ix,
|
|
edge_loop_length)
|
|
edge_loop_ix += edge_loop_length
|
|
|
|
shadow_volume_end_caps(bm,
|
|
bm_position,
|
|
bm_cast_position,
|
|
mesh,
|
|
indicators)
|
|
|
|
link_bmesh(bm, "test")
|
|
|
|
angle = 50
|
|
def rotate():
|
|
global angle
|
|
|
|
torus = bpy.data.objects['Torus']
|
|
torus.rotation_mode = 'XYZ'
|
|
torus.rotation_euler = Euler((0, angle / 100, 0), 'XYZ')
|
|
|
|
try:
|
|
main()
|
|
except:
|
|
import traceback
|
|
for line in traceback.format_exc().split('\n'):
|
|
print(line)
|
|
return
|
|
|
|
angle += 1
|
|
print(angle)
|
|
if angle < 1000:
|
|
bpy.app.timers.register(rotate, first_interval=0.01)
|
|
|
|
#rotate()
|
|
main()
|