#!/usr/bin/env python3 # -*- coding: utf-8 -*- #************************************************************************** # Copyright (C) 2019, Paul Lutus * # * # This program is free software; you can redistribute it and/or modify * # it under the terms of the GNU General Public License as published by * # the Free Software Foundation; either version 2 of the License, or * # (at your option) any later version. * # * # This program is distributed in the hope that it will be useful, * # but WITHOUT ANY WARRANTY; without even the implied warranty of * # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # GNU General Public License for more details. * # * # You should have received a copy of the GNU General Public License * # along with this program; if not, write to the * # Free Software Foundation, Inc., * # 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * #************************************************************************** import sys import os import re import math import argparse try: import bpy except: pass class LensMaker: # user option settings (see also command-line access method) z = 11.8 # float: hyperbolic curvature term segments = 32 # integer rings = 32 # integer # float: lens minor radius depth = .25 # float: lens major radius radius = 1 spherical = False # lens shape: hyperbolic or spherical onesided = False # make symmetrical lens or just one side axis = 'x' # lens orientation in Blender space # redistribute ring interval # for better render quality # this shouldn't normally be changed ring_spacing_factor = 3 # integer # determines overall spherical lens curvature sphericalconst = .25 # end of user settings def get_axis(self,x,y,z,dim): axisdict = { 'x':(x,y,z),'y':(-y,x,z),'z':(z,-y,x) } return axisdict[dim] # interpolation function def ntrp(self,x,xa,xb,ya,yb): return (x-xa) * (yb-ya) / (xb-xa) + ya # see Jupyter notebook "hyperbolic_lens_generation_analysis" def hypi(self,u,z,norm): return math.sqrt(-z+(u+1e-8+math.sqrt(z))**2.0)* norm def sphere_profile(self,x): return math.sqrt(1-x*x) def perform(self): progname = os.path.basename(__file__) parser = argparse.ArgumentParser(prog=progname,description='Purpose: create a Blender mesh object.') parser.add_argument('--rings', dest='rings',type=int,default=self.rings,help='horizontal lines') parser.add_argument('--segments', dest='segments',type=int,default=self.segments,help='radial lines') parser.add_argument('--radius', dest='radius',type=float,default=self.radius,help='lens 1/2 width') parser.add_argument('--depth', dest='depth',type=float,default=self.depth,help='lens 1/2 thickness') parser.add_argument('--axis', dest='axis',type=str,default=self.axis,help='orient lens in x, y or z axis') parser.add_argument('--z', dest='z',type=float,default=self.z,help='hyperbolic curvature factor') parser.add_argument('--spherical', dest='spherical',default=self.spherical, action='store_true',help='spherical lens type (not hyperbolic)') parser.add_argument('--onesided', dest='onesided',default=self.onesided, action='store_true',help='choose surface or lens') blender_present = re.search('(?i)blender',sys.argv[0]) comargs = sys.argv[1:] if len(comargs) == 0: parser.print_help(sys.stderr) i = 0 try: i = comargs.index("--") + 1 except: pass if not blender_present: print("This program is normally used with Blender:") print(" $ blender [optional Blender file] -P %s -- (this program's options)" % progname) args = False try: args = parser.parse_args(comargs[i:]) except: pass if args: result = vars(args) print("Program values: %s" % result) # transfer values to class for item in result: setattr(self, item, result[item]) # normalize factor so hyperbolic # result lies within 0 < y < 1 # only needs to be computed once norm = 1.0/self.hypi(1,self.z,1) sphere_factor = self.sphere_profile(1-self.sphericalconst) verts = [] verts2 = [] faces = [] # generate mesh geometric data for yi in range(self.rings): yv = self.ntrp(yi,0,self.rings-1,0,1) # optimize ring interval yrv = math.pow(yv,self.ring_spacing_factor) if self.spherical: yu = 1-(yrv * self.sphericalconst) yh = self.sphere_profile(yu) * self.radius / sphere_factor else: # hyperbolic yh = self.hypi(yrv,self.z,norm)* self.radius for xi in range(self.segments): a = self.ntrp(xi,0,self.segments,0,math.pi*2) xs = math.sin(a)*yh xc = math.cos(a)*yh y = yrv * self.depth - (self.depth,0)[self.onesided] verts += [self.get_axis(y,xc,xs,self.axis)] # for the two-sided case verts2 += [self.get_axis(-y,xc,xs,self.axis)] verts += verts2 # create face data for w in range((2,1)[self.onesided]): ws = len(verts2) * w # wrap: self.rings # no wrap = self.rings-1 for y in range(self.rings-1): yb = (y+1) % self.rings # wrap: self.segments # no wrap = self.segments-1 for x in range(self.segments): # wrap horizontally xb = (x+1) % self.segments # compute face coordinates a = ws+y*self.segments+x c = ws+yb*self.segments+xb # reverse face construction # direction for opposite side if w > 0: d = ws+y*self.segments +xb b = ws+yb*self.segments +x else: b = ws+y*self.segments +xb d = ws+yb*self.segments +x faces += [[a,b,c,d]] # optional: delete everything #bpy.ops.object.select_all(action='SELECT') #bpy.ops.object.delete() titlestr = ('hyperbolic','spherical')[self.spherical] try: mesh_data = bpy.data.meshes.new("%s_mesh_data" % titlestr) mesh_data.from_pydata(verts, [],faces) mesh_data.update() obj = bpy.data.objects.new("%s_surface" % titlestr, mesh_data) scene = bpy.context.scene scene.collection.objects.link(obj) bpy.ops.object.select_all(action='DESELECT') obj.select_set(True) bpy.context.view_layer.objects.active = obj except: print('No Blender environment libraries detected.') # end of class LensMaker if __name__ == '__main__': LensMaker().perform()