bl_info = { "name": "Mercator Project", "author": "batFINGER", "version": (1, 0), "blender": (2, 79, 0), "location": "View3D > Mesh > UV UnWrap > Mercator Project", "description": "UV Mercator Projection", "warning": "", "wiki_url": "", "category": "UV", } import bpy import bmesh from bpy.props import FloatProperty from math import sin, log, radians, degrees, inf from mathutils import Vector, Matrix class Spherical: def __init__(self, vert, north=(0, 0, 1), long0=(0, -1)): self.vert = vert self.north, self.long0 = Vector(north), Vector(long0) v = vert.co R = v.length lat = radians(90) - self.north.angle(v) is_pole = v.xy.length < 0.000001 sign = 1 if v.x > 0 else -1 long = 0 if is_pole else sign * self.long0.angle(v.xy) self.R = R self.lat = lat self.long = long self.west = long if long < 0 else long - radians(360) self.east = long if long >= 0 else long + radians(360) def __repr__(self): return "%.3f, %.3f, %.3f" % (self.R, degrees(self.lat), degrees(self.long)) class MercatorUV: def x(self, long): ''' mercator longitude mapping ''' return self.R * long def y(self, lat): ''' mercator latitude mapping ''' s = sin(lat) return self.R * log((1 + s) / (1 - s)) / 2 def uv(self, face, uv_layer): ''' Map a UV face ''' # see if face is east / west orient = "long" c = face.calc_center_median() if c.y > 0: # on the "dark side" orient = "west" if c.x < 0 else "east" for l in face.loops: luv = l[uv_layer] # apply the location of the vertex as a UV p = self.pts[l.vert] lat, long = p.lat, getattr(p, orient) luv.uv = self.scale * (self.translate + Vector([self.x(long), p.y])) def calc_uv(self): bm = self.bm uv_layer = bm.loops.layers.uv.verify() bm.faces.layers.tex.verify() # currently blender needs both layers. bm.select_mode = {'FACE', 'VERT'} bm.select_flush_mode() bm.select_flush(True) # adjust UVs for f in bm.faces: if not f.select: continue self.uv(f, uv_layer) pass def __init__(self, me, bm, minlat, maxlat): def pt(v): s = Spherical(v) v.select = minlat <= s.lat <= maxlat setattr(s, "select", v.select) return s self.minlat, self.maxlat = minlat, maxlat # add a new uv map uv = me.uv_textures.new("Mercator") me.uv_textures.active = uv self.bm = bm # spherical coords for verts self.pts = {v: pt(v) for v in self.bm.verts} # radius average of calc'd s.R self.R = sum(s.R for s in self.pts.values()) / len(self.pts) for s in self.pts.values(): if s.select: # set y on a per verf basis , not per face.verts setattr(s, "y", self.y(s.lat)) # scale UV to [0, 1] make scale matrix scale_x = 1 / (self.R * radians(360)) scale_y = 1 / (self.y(maxlat) - self.y(minlat)) self.scale = Matrix([[scale_x, 0], [0, scale_y]]) # and transform vector self.translate = Vector((self.R * radians(180), -self.y(minlat))) class UV_OT_MercatorProject(bpy.types.Operator): """Create a Mercator Projection UV Map""" bl_idname = "uv.mercator_project" bl_label = "Mercator Project" bl_options = {'REGISTER', 'UNDO'} minlat = FloatProperty(default=radians(-82), name="Minimum Latitude", description="Minimum Latitude to project.", min=radians(-86), max=radians(86), precision=2, unit='ROTATION') maxlat = FloatProperty(default=radians(82), name="Minimum Latitude", description="Maximum Latitude to project.", min=radians(-86), max=radians(86), precision=2, unit='ROTATION') @classmethod def poll(cls, context): return (context.mode == 'EDIT_MESH') def execute(self, context): obj = context.edit_object me = obj.data bm = bmesh.from_edit_mesh(me) merc = MercatorUV(me, bm, self.minlat, self.maxlat) merc.calc_uv() bmesh.update_edit_mesh(me) return {'FINISHED'} def unwrapmenu(self, context): ''' menu item ''' self.layout.operator("uv.mercator_project") def register(): # add to edit mesh > UV menu bpy.types.VIEW3D_MT_uv_map.append(unwrapmenu) bpy.utils.register_class(UV_OT_MercatorProject) def unregister(): bpy.types.VIEW3D_MT_uv_map.remove(unwrapmenu) bpy.utils.unregister_class(UV_OT_MercatorProject) if __name__ == "__main__": register()