Tools for working with "non-euclidean" game world maps in Blender
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import bpy, bmesh | |
from mathutils import Color | |
def selectedFaces(bm): | |
return filter(lambda fa: fa.select, bm.faces) | |
def getEditMesh(cx): | |
if cx.mode == 'EDIT_MESH': | |
ob = cx.object | |
if ob and ob.type == 'MESH' and ob.select: | |
return bmesh.from_edit_mesh(ob.data) | |
return None | |
def getPortalMaterial(n): | |
matName = 'EscherPortalMaterial%03d' % n | |
if matName in bpy.data.materials: | |
return bpy.data.materials[matName] | |
mat = bpy.data.materials.new(matName) | |
c = n+1 | |
mat.diffuse_color = Color((c&1, c&2, c&4)) | |
mat.use_transparency = True | |
mat.alpha = 0.5 | |
return mat | |
class OBJECT_OT_NewSpaceButton(bpy.types.Operator): | |
bl_idname = 'escher.create_space' | |
bl_label = 'New Space' | |
def execute(self, cx): | |
# Create a new mesh representing the Esher space | |
# Create a new PSO (Primary Space Object) containing the mesh | |
# Insert PSO into scene | |
# If an EMPTY representing an Escher Remote is active | |
# hide PSO | |
# create SSO (Secondary Space Object) and parent to the EMPTY | |
return {'FINISHED'} | |
def getMeshMaterialByMaterial(me, mat): | |
'''Gets the index of a material in a mesh, and returns it. | |
If it is not already a material of the mesh, it is added.''' | |
if mat.name in me.materials: | |
return me.materials.find(mat.name) | |
rval = len(me.materials) | |
me.materials.append(mat) | |
return rval | |
class ESCHER_OT_Portalize_Face(bpy.types.Operator): | |
'''Portalize selected faces''' | |
bl_idname = "escher.portalize_face" | |
bl_label = "Mesh Face: Portal++" | |
def execute(self, cx): | |
if cx.object.type != 'MESH': | |
raise TypeError("Selected object must be MESH") | |
bm = getEditMesh(cx) | |
if bm: | |
me = cx.object.data | |
matName = None | |
for fa in selectedFaces(bm): | |
mat = me.materials[fa.material_index] | |
if isPortalMaterialName(mat.name): | |
matName = mat.name | |
break | |
if matName: | |
numRemotes = 0 | |
for ob in cx.object.children: | |
if objectIsRemote(ob): | |
numRemotes += 1 | |
remoteIndex = (portalMaterialName2remoteIndex(matName) + 1) % numRemotes | |
else: | |
remoteIndex = 0 | |
mat = getPortalMaterial(remoteIndex) | |
imat = getMeshMaterialByMaterial(me, mat) | |
for fa in selectedFaces(bm): | |
fa.material_index = imat | |
self.report({'INFO'}, 'Portalized face materials!') | |
return {'FINISHED'} | |
@classmethod | |
def poll(cls, cx): | |
me = getEditMesh(cx) | |
if me: | |
fa = list(selectedFaces(me)) | |
if len(fa): | |
return True | |
return False | |
class ESCHER_OT_LinkRemote(bpy.types.Operator): | |
'''Links a remote space to this one''' | |
bl_idname = "escher.link_remote" | |
bl_label = "Link Remote" | |
@classmethod | |
def poll(cls, cx): | |
return cx.mode == 'OBJECT' and cx.object and cx.object.type == 'MESH' | |
def execute(self, cx): | |
pso = cx.object | |
# This empty object represents the remote space | |
eo = bpy.data.objects.new('EscherRemote', None) | |
cx.scene.objects.link(eo) | |
eo['escher_remote_space_name'] = '*none*' | |
eo['escher_portal_index'] = findUnusedPortalIndexInPSO(pso) | |
eo.parent = pso | |
bpy.ops.object.select_all(action='DESELECT') | |
eo.select = True | |
bpy.ops.transform.translate() | |
return bpy.ops.transform.translate('INVOKE_DEFAULT') | |
class ESCHER_OT_RealizeRemote(bpy.types.Operator): | |
'''Creates an object parented to the symbolic EMPTY object representing an Escher Remote. | |
The object created is referred to as an SSO, or "Secondary Space Object." This object is | |
disposable and probably won't get saved in the .blend file. The PSO, or "Primary Space | |
Object," is not expendable, as it has children which are important, and of course its | |
data is the mesh representing the geometry of the Escher Space.''' | |
bl_idname = 'escher.realize_remote' | |
bl_label = 'Realize Remote' | |
@classmethod | |
def poll(cls, cx): | |
return cx.mode == 'OBJECT' and cx.object and cx.object.type == 'EMPTY' and \ | |
'escher_remote_space_name' in cx.object | |
def execute(self, cx): | |
eo = cx.object | |
for child in eo.children: | |
if child.name.startswith('EscherSSO_'): | |
showGraph(child) | |
return {'FINISHED'} | |
remoteSpaceName = eo['escher_remote_space_name'] | |
sso = makeSSO(remoteSpaceName) | |
cx.scene.objects.link(sso) | |
sso.parent = eo | |
# TODO lock all transforms of the SSO | |
return {'FINISHED'} | |
class EscherSelectRemote(bpy.types.Operator): | |
"""Select a remote space to be linked""" | |
bl_idname = "escher.select_remote" | |
bl_label = "Select Remote" | |
bl_options = {'REGISTER', 'UNDO'} | |
bl_property = "enumprop" | |
def item_cb(self, context): | |
return [('*new*', 'New Space', '')] + [(c.name, c.name[10:], '') for c in self.choices] | |
# This has to be a bpy.props.CollectionProperty(), it can't be a Python List!!! | |
choices = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) | |
enumprop = bpy.props.EnumProperty(items=item_cb) | |
@classmethod | |
def poll(cls, cx): | |
return cx.mode == 'OBJECT' and cx.object and cx.object.type == 'EMPTY' and \ | |
'escher_remote_space_name' in cx.object | |
def execute(self, cx): | |
if self.enumprop == '*new*': | |
self.report({'INFO'}, 'TODO: Implement NEW') | |
return {'FINISHED'} | |
remotePsoName = self.enumprop | |
remoteSpaceName = psoName2spaceName(remotePsoName) | |
removeSsosFromRemoteEmpty(cx.object, cx) | |
cx.object['escher_remote_space_name'] = remoteSpaceName | |
return {'FINISHED'} | |
def invoke(self, context, event): | |
self.choices.clear() | |
for ob in bpy.data.objects: | |
if ob.name.startswith('EscherPSO_'): | |
self.choices.add().name = ob.name | |
context.window_manager.invoke_search_popup(self) | |
return {'FINISHED'} | |
class EscherNewSpace(bpy.types.Operator): | |
'''Create a new Escher Space, and focus on its PSO''' | |
bl_idname = 'escher.new_space' | |
bl_label = 'New Space' | |
newSpaceName = bpy.props.StringProperty(name='Escher Space Name', description='Name your new space', default='Unnamed_Space') | |
@classmethod | |
def poll(cls, cx): | |
return cx.mode == 'OBJECT' | |
def execute(self, cx): | |
pso = makePSO(self.newSpaceName) | |
cx.scene.objects.link(pso) | |
self.report({'INFO'}, 'Created new Space, mesh, and PSO') | |
return {'FINISHED'} | |
def invoke(self, cx, ev): | |
return cx.window_manager.invoke_props_dialog(self) | |
def removeSsosFromRemoteEmpty(eo, cx): | |
for o in eo.children: | |
if isSsoName(o.name): | |
cx.scene.unlink(o) | |
def lockAllTransforms(ob): | |
for i in range(3): | |
ob.lock_location[i] = True | |
ob.lock_rotation[i] = True | |
ob.lock_scale[i] = True | |
def makePSO(spaceName, me=None): | |
if me is None: | |
bm = bmesh.new() | |
for x in -1, 1: | |
for y in -1, 1: | |
for z in -1, 1: | |
bm.verts.new().co = (x, y, z) | |
bm.verts.index_update() | |
bm.faces.new((bm.verts[0], bm.verts[2], bm.verts[3], bm.verts[1])) | |
bm.faces.new((bm.verts[4], bm.verts[5], bm.verts[7], bm.verts[6])) | |
bm.faces.new((bm.verts[0], bm.verts[4], bm.verts[6], bm.verts[2])) | |
bm.faces.new((bm.verts[1], bm.verts[3], bm.verts[7], bm.verts[5])) | |
bm.faces.new((bm.verts[0], bm.verts[1], bm.verts[5], bm.verts[4])) | |
bm.faces.new((bm.verts[2], bm.verts[6], bm.verts[7], bm.verts[3])) | |
me = bpy.data.meshes.new(spaceName2meshName(spaceName)) | |
# Assign default material (makes life easier) | |
me.materials.append(bpy.data.materials['Material']) | |
bm.to_mesh(me) | |
pso = bpy.data.objects.new(spaceName2psoName(spaceName), me) | |
pso.show_transparent = True | |
lockAllTransforms(pso) | |
return pso | |
def cloneGraph(o, sc): | |
r = o.copy() | |
sc.objects.link(r) | |
for c in o.children: | |
d = cloneGraph(c, sc) | |
d.parent = r | |
return r | |
def graphWalk(o, fn): | |
fn(o) | |
for c in o.children: | |
graphWalk(c, fn) | |
def makeSso_copy(o, cx): | |
'''This function exists for makeSSO''' | |
# TODO | |
def makeSSO(spaceName): | |
psoName = spaceName2psoName(spaceName) | |
if psoName not in bpy.data.objects: | |
raise KeyError('Could not find "%s"' % psoName) | |
ssoName = spaceName2ssoName(spaceName) | |
sso = bpy.data.objects.new(ssoName, bpy.data.objects[psoName].data) | |
sso.show_transparent = True | |
return sso | |
def findUnusedPortalIndexInPSO(PSO): | |
'''Each Remote Object has a property assigning it to a particular | |
remote index, which corresponds to a particular EscherPortalMaterial. | |
This function is used to find which EscherPortalMaterial should be | |
assigned to a new Remote Object given the PSO it is intended for.''' | |
mats = list() | |
for ob in PSO.children: | |
if ob.type == 'EMPTY' and 'escher_remote_space_name' in ob: | |
if 'escher_portal_index' not in ob: | |
raise KeyError("Escher Remote Object should have an 'escher_portal_index' property!") | |
n = ob['escher_portal_index'] | |
if type(n) is not int: | |
raise TypeError("Escher Remote Object property 'escher_portal_index' is not an int!") | |
if n in mats: | |
raise KeyError("Found duplicate 'escher_portal_index' property among PSO's Remote Objects!") | |
mats.append(n) | |
n = 0 | |
mats.sort() | |
for mat in mats: | |
if n != mat: | |
return n | |
n += 1 | |
return n | |
def portalMaterialName2remoteIndex(matName): | |
if not isPortalMaterialName(matName): | |
raise ValueError('Invalid Escher Portal Material name') | |
s = matName[20:] | |
if len(s): | |
return int(s) | |
return 0 | |
def objectIsRemote(o): | |
return o.type == 'EMPTY' and o.name.startswith('EscherRemote') | |
def isPortalMaterialName(matName): | |
return matName.startswith('EscherPortalMaterial') | |
def isUnqualifiedSpaceName(spaceName): | |
return not (spaceName.startswith('EscherPSO_') or spaceName.startswith('EscherSSO_') or spaceName.startswith('EscherSM_')) | |
def toUnqualifiedSpaceName(spaceName): | |
if isUnqualifiedSpaceName(spaceName): | |
return spaceName | |
return spaceName[spaceName.find('_')+1:] | |
def spaceName2meshName(spaceName): | |
if isUnqualifiedSpaceName(spaceName): | |
return 'EscherSM_' + spaceName | |
raise ValueError('Invalid space name!') | |
def spaceName2psoName(spaceName): | |
if isUnqualifiedSpaceName(spaceName): | |
return 'EscherPSO_' + spaceName | |
raise ValueError('Invalid space name!') | |
def spaceName2ssoName(spaceName): | |
if isUnqualifiedSpaceName(spaceName): | |
return 'EscherSSO_' + spaceName | |
raise ValueError('Invalid space name!') | |
def psoName2spaceName(psoName): | |
if psoName.startswith('EscherPSO_'): | |
return psoName[10:] | |
else: | |
raise ValueError('Invalid Escher PSO name string') | |
class EscherFocusSpace(bpy.types.Operator): | |
'''Focus on a space by hiding all other spaces and displaying one space's PSO''' | |
bl_idname = 'escher.focus_space' | |
bl_label = 'Focus Space' | |
bl_property = "enumprop" | |
def item_cb(self, cx): | |
rval = [(c.name, c.name[10:], '') for c in self.choices] | |
if cx.object: | |
rval = [('*current*', 'Currently Selected Object', '')] + rval | |
return rval | |
# This has to be a bpy.props.CollectionProperty(), it can't be a Python List!!! | |
choices = bpy.props.CollectionProperty(type=bpy.types.PropertyGroup) | |
enumprop = bpy.props.EnumProperty(items=item_cb) | |
@classmethod | |
def poll(cls, cx): | |
return cx.mode == 'OBJECT' | |
def execute(self, cx): | |
if self.enumprop == '*current*': | |
psoName = spaceName2psoName(toUnqualifiedSpaceName(cx.object.name)) | |
else: | |
psoName = self.enumprop | |
for ob in cx.scene.objects: | |
if ob.type == 'MESH': | |
hideGraph(ob) | |
pso = bpy.data.objects[psoName] | |
showGraph(pso) | |
return {'FINISHED'} | |
def invoke(self, context, event): | |
self.choices.clear() | |
for ob in bpy.data.objects: | |
if ob.name.startswith('EscherPSO_'): | |
self.choices.add().name = ob.name | |
context.window_manager.invoke_search_popup(self) | |
return {'FINISHED'} | |
def selectObject(o): | |
o.select = True | |
class EscherDeepCopy(bpy.types.Operator): | |
'''Copies a portion of the scene graph''' | |
bl_idname = 'escher.deep_copy' | |
bl_label = 'Deep Copy' | |
@classmethod | |
def poll(cls, cx): | |
return cx.mode == 'OBJECT' and cx.object | |
def execute(self, cx): | |
o = cloneGraph(cx.object, cx.scene) | |
bpy.ops.object.select_all(action='DESELECT') | |
graphWalk(o, selectObject) | |
return {'FINISHED'} | |
class EscherDeselectPortalFaces(bpy.types.Operator): | |
'''Deselects portal faces''' | |
bl_idname = 'escher.deselect_portals' | |
bl_label = 'Deslect Portal Faces' | |
@classmethod | |
def poll(cls, cx): | |
return cx.mode == 'EDIT_MESH' and cx.object.type == 'MESH' | |
def execute(self, cx): | |
me = cx.object.data | |
em = getEditMesh(cx) | |
for f in em.faces: | |
if isPortalMaterialName(me.materials[f.material_index].name): | |
f.select = False | |
return {'FINISHED'} | |
def showGraph(ob): | |
hideGraph(ob, False) | |
def hideGraph(ob, hide=True): | |
ob.hide = hide | |
for o in ob.children: | |
hideGraph(o, hide) | |
class NPanel(bpy.types.Panel): | |
bl_label = 'Escher Tools' | |
bl_space_type = 'VIEW_3D' | |
bl_region_type = 'TOOLS' | |
def draw(self, cx): | |
layout = self.layout | |
layout.operator('escher.portalize_face') | |
layout.operator('escher.link_remote') | |
layout.operator('escher.realize_remote') | |
layout.operator('mesh.flip_normals', text='Flip Normals') | |
layout.operator('escher.select_remote') | |
layout.operator('escher.new_space') | |
layout.operator('escher.focus_space') | |
layout.operator('escher.deep_copy') | |
layout.operator('escher.deselect_portals') | |
def register(): | |
#bpy.types.Object.escher_space_name = bpy.props.EnumProperty(items=escher_space_names) | |
bpy.types.Object | |
bpy.utils.register_module(__name__) | |
def unregister(): | |
#del bpy.types.Object.escher_space_name | |
bpy.utils.unregister_module(__name__) | |
if __name__ == '__main__': | |
register() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment