metaforce/hecl/blender/hecl/path.py

389 lines
14 KiB
Python

import bpy, gpu, sys, bmesh, struct
from mathutils import Vector
from gpu_extras.batch import batch_for_shader
# Convenience class that automatically brings active edit mesh's face into scope for get/set
class HeightRef:
def __init__(self):
self.bm = None
context = bpy.context
obj = context.scene.objects[context.scene.hecl_path_obj]
if obj.type != 'MESH':
return
if context.edit_object != obj:
return
bm = bmesh.from_edit_mesh(obj.data)
if 'Height' not in bm.faces.layers.float:
return
self.height_lay = bm.faces.layers.float['Height']
self.bm = bm
@property
def ready(self):
return self.bm is not None and self.bm.faces.active is not None
@property
def value(self):
if self.ready:
return self.bm.faces.active[self.height_lay]
@value.setter
def value(self, value):
if self.ready:
for f in self.bm.faces:
if f.select:
f[self.height_lay] = value
# Active edit face height get
def get_height(self):
return HeightRef().value
# Selected edit face(s) height set
def set_height(self, val):
HeightRef().value = val
for ar in bpy.context.screen.areas:
if ar.type == 'VIEW_3D':
ar.tag_redraw()
# Simple AABB class
class AABB:
def __init__(self):
self.min = Vector((999999.0, 999999.0, 999999.0))
self.max = Vector((-999999.0, -999999.0, -999999.0))
def accumulate(self, vec):
for i in range(3):
if vec[i] < self.min[i]:
self.min[i] = vec[i]
if vec[i] > self.max[i]:
self.max[i] = vec[i]
def isValid(self):
for i in range(3):
if self.min[i] > self.max[i]:
return False
return True
# Simple adjacency calculator
class AdjacencySet:
def __init__(self, bm, mesh_obj, type_mask):
self.faces = {}
for f in bm.faces:
material = mesh_obj.material_slots[f.material_index].material
if (material.retro_path_type_mask & type_mask) == 0:
continue
face_set = set()
face_set.add(f)
# Breadth-first loop to avoid crashing python with large recursion
next_level = [f]
while len(next_level):
next_next_level = []
for of in next_level:
for e in of.edges:
for of2 in e.link_faces:
if of2 == of:
continue
if of2 not in face_set:
material = mesh_obj.material_slots[of2.material_index].material
if material.retro_path_type_mask & type_mask:
face_set.add(of2)
next_next_level.append(of2)
next_level = next_next_level
self.faces[f] = face_set
def has_adjacency(self, face_a, face_b):
if face_a not in self.faces:
return False
return face_b in self.faces[face_a]
# Cooking entry point
def cook(writebuf, mesh_obj):
ba = bytearray()
# Version 4
ba += struct.pack('>I', 4)
bm = bmesh.new()
bm.from_mesh(mesh_obj.data)
bm.faces.ensure_lookup_table()
height_lay = None
if 'Height' in bm.faces.layers.float:
height_lay = bm.faces.layers.float['Height']
# Gather immediate adjacencies
node_list = []
link_list = []
region_list = []
up_vec = Vector((0.0, 0.0, 1.0))
for f in bm.faces:
start_loop = f.loops[0]
cur_loop = start_loop
node_idx = 0
start_node = len(node_list)
start_link = len(link_list)
while True:
node_list.append([cur_loop, up_vec])
nv1 = cur_loop.vert.co
cur_loop = cur_loop.link_loop_prev
nv0 = cur_loop.vert.co
node_list[-1][1] = (nv0 - nv1).cross(up_vec).normalized()
for other_face in cur_loop.edge.link_faces:
if other_face == f:
continue
link_list.append((node_idx, other_face.index, cur_loop.edge.calc_length()))
node_idx += 1
if cur_loop == start_loop:
break
region_list.append((f, range(start_node, len(node_list)), range(start_link, len(link_list))))
# Emit nodes
ba += struct.pack('>I', len(node_list))
for n in node_list:
v = n[0].vert
normal = n[1]
ba += struct.pack('>ffffff', v.co[0], v.co[1], v.co[2], normal[0], normal[1], normal[2])
# Emit links
ba += struct.pack('>I', len(link_list))
for l in link_list:
ba += struct.pack('>IIff', l[0], l[1], l[2], 1.0 / l[2])
# Emit regions
ba += struct.pack('>I', len(region_list))
for r in region_list:
material = mesh_obj.material_slots[r[0].material_index].material
height = 1.0
if height_lay is not None:
height = r[0][height_lay]
center = r[0].calc_center_median_weighted()
aabb = AABB()
for v in r[0].verts:
aabb.accumulate(v.co)
aabb.accumulate(v.co + Vector((0.0, 0.0, height)))
ba += struct.pack('>IIIIHHffffIfffffffffI', len(r[1]), r[1].start, len(r[2]), r[2].start,
material.retro_path_idx_mask, material.retro_path_type_mask,
height, r[0].normal[0], r[0].normal[1], r[0].normal[2],
r[0].index, center[0], center[1], center[2],
aabb.min[0], aabb.min[1], aabb.min[2],
aabb.max[0], aabb.max[1], aabb.max[2],
r[0].index)
num_regions = len(region_list)
total_adjacencies = num_regions * (num_regions - 1) // 2
num_words = (total_adjacencies + 31) // 32
# Find ground adjacencies
words = [0] * num_words
ground_adjacencies = AdjacencySet(bm, mesh_obj, 0x1)
for i in range(num_regions):
for j in range(num_regions):
if i == j:
continue
i1 = i
i2 = j
if i1 > i2:
continue
if ground_adjacencies.has_adjacency(bm.faces[i], bm.faces[j]):
rem_regions = num_regions - i1
rem_connections = rem_regions * (rem_regions - 1) // 2
bit = total_adjacencies - rem_connections + i2 - (i1 + 1)
words[bit // 32] |= 1 << (bit % 32)
for w in words:
ba += struct.pack('>I', w)
# Find flyer adjacencies
words = [0] * num_words
flyer_adjacencies = AdjacencySet(bm, mesh_obj, 0x3)
for i in range(num_regions):
for j in range(num_regions):
if i == j:
continue
i1 = i
i2 = j
if i1 > i2:
continue
if flyer_adjacencies.has_adjacency(bm.faces[i], bm.faces[j]):
rem_regions = num_regions - i1
rem_connections = rem_regions * (rem_regions - 1) // 2
bit = total_adjacencies - rem_connections + i2 - (i1 + 1)
words[bit // 32] |= 1 << (bit % 32)
for w in words:
ba += struct.pack('>I', w)
# Unused zero bits
for i in range((((num_regions * num_regions) + 31) // 32 - num_words) * 2):
ba += struct.pack('>I', 0)
# Empty octree (to be generated by hecl)
ba += struct.pack('>II', 0, 0)
# Write out
writebuf(struct.pack('I', len(ba)))
writebuf(ba)
try:
line_shader = gpu.shader.from_builtin('3D_FLAT_COLOR')
except:
pass
# Line draw helper
def draw_line_3d(pos_vbo, color_vbo, color, start, end):
pos_vbo.append(start)
pos_vbo.append(end)
color_vbo.append(color)
color_vbo.append(color)
# Draw RNA polygon
def draw_poly(pos_vbo, color_vbo, p, obj, obj_mtx, height, top_color, side_color):
for ek in p.edge_keys:
co0 = obj_mtx @ obj.data.vertices[ek[0]].co
co1 = obj_mtx @ obj.data.vertices[ek[1]].co
draw_line_3d(pos_vbo, color_vbo, top_color, co0 + Vector((0.0, 0.0, height)),
co1 + Vector((0.0, 0.0, height)))
for vk in p.vertices:
co = obj_mtx @ obj.data.vertices[vk].co
draw_line_3d(pos_vbo, color_vbo, side_color, co, co + Vector((0.0, 0.0, height)))
# Draw bmesh face
def draw_face(pos_vbo, color_vbo, f, obj_mtx, height, top_color, side_color):
for e in f.edges:
co0 = obj_mtx @ e.verts[0].co
co1 = obj_mtx @ e.verts[1].co
draw_line_3d(pos_vbo, color_vbo, top_color, co0 + Vector((0.0, 0.0, height)),
co1 + Vector((0.0, 0.0, height)))
for v in f.verts:
co = obj_mtx @ v.co
draw_line_3d(pos_vbo, color_vbo, side_color, co, co + Vector((0.0, 0.0, height)))
# Viewport hook callback
def draw_callback_3d(self, context):
# object locations
if context.scene.hecl_path_obj not in context.scene.objects:
return
obj = context.scene.objects[context.scene.hecl_path_obj]
if obj.type != 'MESH':
return
obj_mtx = obj.matrix_world
bm = None
height_lay = None
if obj == context.edit_object:
bm = bmesh.from_edit_mesh(obj.data)
if 'Height' in bm.faces.layers.float:
height_lay = bm.faces.layers.float['Height']
else:
if 'Height' in obj.data.polygon_layers_float:
height_lay = obj.data.polygon_layers_float['Height']
pos_vbo = []
color_vbo = []
# Deselected colors
top_color = (0.0, 0.0, 1.0, 0.7)
side_color = (1.0, 0.0, 0.0, 0.7)
if bm is not None:
for f in bm.faces:
height = 1.0
if height_lay is not None:
selected = f.select
if selected:
continue
height = f[height_lay]
draw_face(pos_vbo, color_vbo, f, obj_mtx, height, top_color, side_color)
else:
for p in obj.data.polygons:
height = 1.0
if height_lay is not None:
height = height_lay.data[p.index].value
draw_poly(pos_vbo, color_vbo, p, obj, obj_mtx, height, top_color, side_color)
# Selected colors
if bm is not None:
top_color = (1.0, 0.0, 1.0, 0.7)
side_color = (0.0, 1.0, 0.0, 0.7)
# Avoid z-fighting on selected lines
#bgl.glDepthRange(-0.001, 0.999)
for f in bm.faces:
if height_lay is not None:
selected = f.select
if not selected:
continue
height = f[height_lay]
draw_face(pos_vbo, color_vbo, f, obj_mtx, height, top_color, side_color)
#bgl.glEnd()
#bgl.glDepthRange(0.0, 1.0)
line_shader.bind()
batch = batch_for_shader(line_shader, 'LINES', {"pos": pos_vbo, "color": color_vbo})
batch.draw(line_shader)
# Toggle height viz button
class PathHeightDrawOperator(bpy.types.Operator):
bl_idname = "view3d.toggle_path_height_visualization"
bl_label = "Toggle PATH height visualization"
_handle_3d = None
def execute(self, context):
# the arguments we pass the the callback
args = (self, context)
# Add the region OpenGL drawing callback
# draw in view space with 'POST_VIEW' and 'PRE_VIEW'
if self._handle_3d is None:
PathHeightDrawOperator._handle_3d = \
bpy.types.SpaceView3D.draw_handler_add(draw_callback_3d, args, 'WINDOW', 'POST_VIEW')
else:
bpy.types.SpaceView3D.draw_handler_remove(PathHeightDrawOperator._handle_3d, 'WINDOW')
PathHeightDrawOperator._handle_3d = None
for ar in bpy.context.screen.areas:
if ar.type == 'VIEW_3D':
ar.tag_redraw()
return {'FINISHED'}
# Toggle background wire button
class PathBackgroundWireframeOperator(bpy.types.Operator):
bl_idname = "view3d.toggle_path_background_wireframe"
bl_label = "Toggle PATH background wireframe"
def execute(self, context):
if context.scene.background_set:
to_wire = False
for o in context.scene.background_set.objects:
if o.display_type != 'WIRE':
to_wire = True
break
if to_wire:
for o in context.scene.background_set.objects:
o.display_type = 'WIRE'
else:
for o in context.scene.background_set.objects:
o.display_type = 'TEXTURED'
return {'FINISHED'}
# Edit panel
def draw(layout, context):
layout.prop_search(context.scene, 'hecl_path_obj', context.scene, 'objects')
layout.operator('view3d.toggle_path_background_wireframe', text='Toggle Background Wire', icon='SHADING_WIRE')
if PathHeightDrawOperator._handle_3d:
icon = 'HIDE_OFF'
else:
icon = 'HIDE_ON'
layout.operator('view3d.toggle_path_height_visualization', text='Toggle Height Viz', icon=icon)
if HeightRef().ready:
layout.prop(context.window_manager, 'hecl_height_prop', text='Height')
# Registration
def register():
bpy.types.Material.retro_path_idx_mask = bpy.props.IntProperty(name='Retro: Path Index Mask')
bpy.types.Material.retro_path_type_mask = bpy.props.IntProperty(name='Retro: Path Type Mask')
bpy.types.WindowManager.hecl_height_prop = bpy.props.FloatProperty(
get=get_height, set=set_height, options={'HIDDEN'})
bpy.types.Scene.hecl_path_obj = bpy.props.StringProperty(
name='HECL Path Object',
description='Blender Mesh Object to export during PATH\'s cook process')
bpy.utils.register_class(PathHeightDrawOperator)
bpy.utils.register_class(PathBackgroundWireframeOperator)
def unregister():
bpy.utils.unregister_class(PathHeightDrawOperator)
bpy.utils.unregister_class(PathBackgroundWireframeOperator)