dreamcast/blender_shadow_volume2.py

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()