├── .gitignore ├── eyemodel ├── textures │ ├── teeth.png │ ├── spec map.png │ ├── ireye-dark.png │ ├── head normal.png │ ├── ir color map.png │ └── ireye-light.png ├── Swirski-EyeModel.blend ├── __init__.py └── blender_script.py.template ├── LICENSE └── example.py /.gitignore: -------------------------------------------------------------------------------- 1 | example.m 2 | example.png 3 | *.pyc 4 | eyemodel/textures/ireye.png 5 | -------------------------------------------------------------------------------- /eyemodel/textures/teeth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeszekSwirski/eyemodel/HEAD/eyemodel/textures/teeth.png -------------------------------------------------------------------------------- /eyemodel/textures/spec map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeszekSwirski/eyemodel/HEAD/eyemodel/textures/spec map.png -------------------------------------------------------------------------------- /eyemodel/Swirski-EyeModel.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeszekSwirski/eyemodel/HEAD/eyemodel/Swirski-EyeModel.blend -------------------------------------------------------------------------------- /eyemodel/textures/ireye-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeszekSwirski/eyemodel/HEAD/eyemodel/textures/ireye-dark.png -------------------------------------------------------------------------------- /eyemodel/textures/head normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeszekSwirski/eyemodel/HEAD/eyemodel/textures/head normal.png -------------------------------------------------------------------------------- /eyemodel/textures/ir color map.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeszekSwirski/eyemodel/HEAD/eyemodel/textures/ir color map.png -------------------------------------------------------------------------------- /eyemodel/textures/ireye-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeszekSwirski/eyemodel/HEAD/eyemodel/textures/ireye-light.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Lech Swirski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=utf-8 3 | 4 | import eyemodel 5 | 6 | # ^ 7 | # | .-. 8 | # | | | <- Head 9 | # | `^u^' 10 | # Y | ¦V <- Camera (As seen from above) 11 | # | ¦ 12 | # | ¦ 13 | # | o <- Target 14 | # 15 | # ----------> X 16 | # 17 | # +X = left 18 | # +Y = back 19 | # +Z = up 20 | 21 | with eyemodel.Renderer() as r: 22 | r.eye_target = [0, -1000, 0] 23 | r.camera_position = [20, -50, -10] 24 | r.camera_target = [0, -r.eye_radius, 0] 25 | r.eye_closedness = 0.2 26 | r.iris = "light" 27 | 28 | r.lights = [ 29 | eyemodel.Light( 30 | type="sun", 31 | location = [10, -10, 100], 32 | strength = 10, 33 | target = [0,0,0]), 34 | eyemodel.Light( 35 | strength = 1, 36 | location = [15, -50, -10], 37 | target = r.camera_target), 38 | eyemodel.Light( 39 | strength = 1, 40 | location = [25, -50, -10], 41 | target = r.camera_target) 42 | ] 43 | 44 | r.render_samples = 50 45 | r.render("example.png", "example.m") 46 | -------------------------------------------------------------------------------- /eyemodel/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=utf-8 3 | 4 | import sys 5 | import os 6 | import math 7 | import collections 8 | import subprocess 9 | import tempfile 10 | import shutil 11 | import time 12 | import traceback 13 | import threading 14 | import re 15 | import random 16 | import signal 17 | import struct 18 | try: 19 | from Queue import Queue, Empty 20 | except ImportError: 21 | from queue import Queue, Empty # python 3.x 22 | 23 | SCRIPT_PATH = sys.arg[0] if __name__ == "__main__" else __file__ 24 | SCRIPT_DIR = os.path.dirname(SCRIPT_PATH) 25 | 26 | MODEL_PATH = os.path.join(SCRIPT_DIR, "Swirski-EyeModel.blend") 27 | TEXTURE_PATH = os.path.join(SCRIPT_DIR, "textures") 28 | BLENDER_SCRIPT_TEMPLATE = os.path.join(SCRIPT_DIR, "blender_script.py.template") 29 | 30 | RENDER_LINE_RE = re.compile(r""" 31 | Fra: \s* (?P\d+) \s* # Frame number 32 | Mem: \s* (?P[\d.]+\S) \s* # Memory use 33 | \( 34 | [\d.]+\S \s*,\s* 35 | Peak \s* (?P[\d.]+\S) # Peak memory 36 | \) 37 | \s*\|\s* 38 | Remaining: \s* (?P[\d:.]+) # Remaining time 39 | \s*\|\s* 40 | Mem: \s* (?P[\d.]+\S) \s*,\s* # More memory use? 41 | Peak: \s* (?P[\d.]+\S) # And more peak memory? 42 | \s*\|\s* 43 | (?P[^,|]+) \s*,\s* (?P[^,|]+) # Rig and layer name 44 | \s*\|\s* 45 | Path\ Tracing\ Tile \s+ 46 | (?P\d+)/(?P\d+) # Tile num 47 | \s*,\s* 48 | Sample \s+ 49 | (?P\d+)/(?P\d+) # Sample num 50 | \s* 51 | """, flags=re.VERBOSE) 52 | 53 | 54 | def get_blender_path(): 55 | def isexecutable(path): 56 | return os.path.isfile(path) and os.access(path, os.X_OK) 57 | 58 | # Get blender from environment if it's set 59 | if "BLENDER_PATH" in os.environ: 60 | path = os.environ["BLENDER_PATH"] 61 | if isexecutable(path): 62 | return path 63 | 64 | # If blender is on the path, just rely on the OS's path search 65 | if isexecutable("blender"): 66 | return "blender" 67 | 68 | # Try some default values 69 | if sys.platform == "win32": 70 | paths = ["C:/Program Files/Blender Foundation/Blender/blender.exe", 71 | "C:/Program Files (x86)/Blender Foundation/Blender/blender.exe"] 72 | else: 73 | paths = ["/usr/local/bin/blender", "/usr/bin/blender", "/bin/blender", 74 | "/Applications/Blender/blender.app/Contents/MacOS/blender"] 75 | 76 | for path in paths: 77 | if isexecutable(path): 78 | return path 79 | 80 | raise Exception("Blender not found, try setting the BLENDER_PATH environment variable") 81 | 82 | def check_blender_version(): 83 | blender_path = get_blender_path() 84 | result = subprocess.run([blender_path, "--version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE) 85 | version_output = result.stdout.decode("utf-8") 86 | version_match = re.search(r"Blender\s+(\d+\.\d+)", version_output) 87 | if version_match: 88 | version = version_match.group(1) 89 | if not version.startswith("4."): 90 | print(f"Blender version {version} detected. Please upgrade to Blender 4.x.") 91 | else: 92 | print("Unable to determine Blender version. Please ensure Blender is installed correctly.") 93 | 94 | 95 | class Light(collections.namedtuple('Light', ["location", "target", "type", "size", "strength", "view_angle"])): 96 | def __new__(cls, location, target, type="spot", size=2, strength=2, view_angle=45): 97 | return super(Light, cls).__new__(cls, location, target, type, size, strength, view_angle) 98 | 99 | 100 | class Renderer(): 101 | """Renderer. 102 | 103 | ^ 104 | | .-. 105 | | | | <- Head 106 | | `^u^' 107 | Y | ¦V <- Camera (As seen from above) 108 | | ¦ 109 | | ¦ 110 | | o <- Target 111 | 112 | ----------> X 113 | 114 | +X = left 115 | +Y = back 116 | +Z = up 117 | """ 118 | 119 | _renderer_active = False 120 | 121 | def __enter__(self): 122 | return self 123 | 124 | def __exit__(self, type, value, traceback): 125 | Renderer._renderer_active = False 126 | 127 | def __init__(self): 128 | check_blender_version() 129 | Renderer._renderer_active = True 130 | 131 | self.eye_radius = 24/2 132 | self.eye_position = [0,0,0] 133 | self.eye_target = [0,-1000,0] 134 | self.eye_up = [0,0,1] 135 | self.eye_closedness = 0.0 136 | 137 | self.iris = "dark" 138 | 139 | self.cornea_refractive_index = 1.336 140 | 141 | self.pupil_radius = 4/2 142 | 143 | self.camera_position = None 144 | self.camera_target = None 145 | self.camera_up = [0,0,1] 146 | 147 | self.image_size = (640, 480) 148 | self.focal_length = (640/2.0) / math.tan(45*math.pi/180 / 2) 149 | self.focus_distance = None 150 | 151 | self.fstop = 2.0 152 | 153 | self.lights = [] 154 | 155 | self.render_samples = 20 156 | self.render_seed = None 157 | 158 | self.camera_noise_seed = None 159 | 160 | def render(self, path, params=None, background=True, cuda=True, attempts=5): 161 | __, ext = os.path.splitext(path) 162 | ext = ext.lower() 163 | if ext == ".png": 164 | render_format = "PNG" 165 | elif ext == ".jpg" or ext == ".jpeg": 166 | render_format = "JPEG" 167 | elif ext == ".bmp": 168 | render_format = "BMP" 169 | else: 170 | raise RuntimeError("Path extension needs to be one of png, jpg or bmp") 171 | 172 | new_ireye_path = os.path.join(TEXTURE_PATH, "ireye-{}.png".format(self.iris)) 173 | if not os.path.exists(new_ireye_path): 174 | raise RuntimeError("Eye texture {} does not exist. Create one in the textures folder.".format("ireye-{}.png".format(self.iris))) 175 | 176 | with open(BLENDER_SCRIPT_TEMPLATE) as blender_script_template_file: 177 | blender_script_template = blender_script_template_file.read() 178 | blender_script_template = blender_script_template.replace("{","{{") 179 | blender_script_template = blender_script_template.replace("}","}}") 180 | blender_script_template = blender_script_template.replace("$INPUTS","{}") 181 | 182 | if self.camera_position is None: 183 | raise RuntimeError("Camera position not set") 184 | if self.camera_target is None: 185 | raise RuntimeError("Camera target not set") 186 | if len(self.lights) == 0: 187 | print("WARNING: No lights in scene") 188 | 189 | if self.focus_distance is None: 190 | focus_distance = math.sqrt(sum((a-b)**2 for a,b in zip(self.camera_position, self.camera_target))) 191 | else: 192 | focus_distance = self.focus_distance 193 | 194 | maxint = int(2**(struct.calcsize("i")*8-1) - 1) 195 | 196 | if self.render_seed is None: 197 | render_seed = random.randint(0, maxint) 198 | else: 199 | render_seed = self.render_seed 200 | 201 | if self.camera_noise_seed is None: 202 | camera_noise_seed = random.randint(0, maxint) 203 | else: 204 | camera_noise_seed = self.camera_noise_seed 205 | 206 | try: 207 | with tempfile.NamedTemporaryFile("w", suffix=".log", delete=False) as blender_err_file: 208 | blender_err_file_name = blender_err_file.name 209 | 210 | inputs = { 211 | "input_use_cuda": cuda, 212 | "input_eye_radius": self.eye_radius, 213 | "input_eye_pos": "Vector({})".format(list(self.eye_position)), 214 | "input_eye_target": "Vector({})".format(list(self.eye_target)), 215 | "input_eye_up": "Vector({})".format(list(self.eye_up)), 216 | "input_eye_closedness": self.eye_closedness, 217 | 218 | "input_iris": "'{}'".format(self.iris), 219 | 220 | "input_eye_cornea_refrative_index": self.cornea_refractive_index, 221 | 222 | "input_pupil_radius": self.pupil_radius, 223 | 224 | "input_cam_pos": "Vector({})".format(list(self.camera_position)), 225 | "input_cam_target": "Vector({})".format(list(self.camera_target)), 226 | "input_cam_up": "Vector({})".format(list(self.camera_up)), 227 | 228 | "input_cam_image_size": list(self.image_size), 229 | "input_cam_focal_length": self.focal_length, 230 | "input_cam_focus_distance": focus_distance, 231 | "input_cam_fstop": self.fstop, 232 | 233 | "input_lights": ["Light({})".format( 234 | ",".join("{}={}".format(k,v) for k,v in 235 | { 236 | "location": "Vector({})".format(list(l.location)), 237 | "target": "Vector({})".format(list(l.target)), 238 | "type": '"{}"'.format(l.type), 239 | "size": l.size, 240 | "strength": l.strength, 241 | "view_angle": l.view_angle 242 | }.items())) for l in self.lights], 243 | 244 | "input_render_samples" : self.render_samples, 245 | "input_render_seed" : render_seed, 246 | "input_camera_noise_seed" : camera_noise_seed, 247 | "output_render_path" : "'{}'".format(path).replace("\\","/"), 248 | } 249 | if params: 250 | inputs["output_params_path"] = "'{}'".format(params).replace("\\","/") 251 | else: 252 | inputs["output_params_path"] = "None" 253 | 254 | def inputVal(v): 255 | if isinstance(v,list): 256 | return '[{}]'.format(",".join(inputVal(x) for x in v)) 257 | else: 258 | return str(v) 259 | 260 | blender_script = blender_script_template.format("\n".join( 261 | "{} = {}".format(k,inputVal(v)) for k,v in inputs.items())) 262 | blender_script = "\n".join(" " + x for x in blender_script.split("\n")) 263 | blender_script = ("import sys\ntry:\n" + blender_script + 264 | "\nexcept:" 265 | "\n import traceback" 266 | "\n with open(r'" + blender_err_file.name + "','a') as f:" 267 | "\n f.write('\\n'.join(traceback.format_exception(*sys.exc_info())))" 268 | "\n sys.exit(1)") 269 | 270 | with tempfile.NamedTemporaryFile("w", suffix=".py", delete=False) as blender_script_file: 271 | blender_script_file.write(blender_script) 272 | 273 | try: 274 | with tempfile.NamedTemporaryFile(suffix="0000", delete=False) as blender_outfile: 275 | pass 276 | 277 | blender_args = [get_blender_path(), 278 | MODEL_PATH, # Load model 279 | "--enable-autoexec", # Automatic python script execution 280 | "--verbose", "0", # No debug output 281 | "--python", blender_script_file.name, # Run the temporary blender script file 282 | "-o", blender_outfile.name[:-4]+"####", # Render output to temporary file 283 | "--render-format", render_format, # Set render format (e.g. Jpeg) 284 | "-noaudio", # Don't use audio 285 | "--use-extension", "0",] # Don't append the file extension 286 | if background: 287 | blender_args += ["--background"] # Load the file in the background (no UI) 288 | if self.render_samples > 0: 289 | blender_args += ["--render-frame", "0"] # Render frame 0 290 | 291 | # Write script to error log 292 | with open(blender_err_file_name, "a") as blender_err_file: 293 | if sys.platform == 'win32': 294 | blender_err_file.write(subprocess.list2cmdline(blender_args)) 295 | else: 296 | blender_err_file.write(" ".join('"{}"'.format(arg) if " " in arg else arg 297 | for arg in blender_args)) 298 | 299 | blender_err_file.write("\n\n") 300 | 301 | blender_err_file.write("{0}:\n".format(blender_script_file.name)) 302 | blender_err_file.write("------\n") 303 | blender_err_file.write("\n".join( 304 | "{: 4} | {}".format(i+1,x) 305 | for i,x in enumerate(blender_script.split("\n")))) 306 | blender_err_file.write("\n------\n") 307 | 308 | attempts = max(attempts,1) 309 | for attempt in range(1, 1 + attempts): 310 | try: 311 | def enqueue_output(out, queue, name): 312 | for line in iter(out.readline, b''): 313 | line = line.rstrip() 314 | if sys.version_info.major >= 3: 315 | line = line.decode("utf-8") 316 | queue.put((name, line)) 317 | out.close() 318 | 319 | if hasattr(os.sys, 'winver'): 320 | p = subprocess.Popen(blender_args, bufsize=0, stdout=subprocess.PIPE, stderr=subprocess.PIPE, creationflags=subprocess.CREATE_NEW_PROCESS_GROUP) 321 | else: 322 | p = subprocess.Popen(blender_args, bufsize=0, stdout=subprocess.PIPE, stderr=subprocess.PIPE) 323 | 324 | q = Queue() 325 | tout = threading.Thread(target=enqueue_output, args=(p.stdout, q, "out")) 326 | tout.daemon = True # thread dies with the program 327 | terr = threading.Thread(target=enqueue_output, args=(p.stderr, q, "err")) 328 | terr.daemon = True # thread dies with the program 329 | tout.start() 330 | terr.start() 331 | 332 | print("Starting blender") 333 | 334 | # while tout.isAlive() or terr.isAlive(): # deprecated in python 3.9 335 | while tout.is_alive() or terr.is_alive(): 336 | try: 337 | line = q.get(timeout=0.1) 338 | except Empty: 339 | pass 340 | else: 341 | if line[0] == "out": 342 | m = RENDER_LINE_RE.match(line[1]) 343 | if m: 344 | tile = int(m.group("tile")) 345 | tiles = int(m.group("tiles")) 346 | sample = int(m.group("sample")) 347 | samples = int(m.group("samples")) 348 | print("Rendered {percent}%, time remaining: {rem} (tile {tile}/{tiles}, sample {sample}/{samples})".format( 349 | percent=100 * ((tile-1)*samples + (sample-1)) / (tiles*samples), 350 | **m.groupdict() 351 | )) 352 | 353 | with open(blender_err_file_name, "a") as blender_err_file: 354 | blender_err_file.write(line[0]) 355 | blender_err_file.write(" | ") 356 | blender_err_file.write(line[1]) 357 | blender_err_file.write("\n") 358 | 359 | if p.wait() != 0: 360 | print("Blender error") 361 | raise subprocess.CalledProcessError(p.returncode, blender_args) 362 | 363 | print("Blender quit") 364 | break 365 | 366 | except KeyboardInterrupt: 367 | raise 368 | except: 369 | # Sometimes blender fails in rendering, so retry until success 370 | traceback.print_exc() 371 | if attempt < attempts: 372 | print("Blender call failed, retrying in 1 sec (attempt {} of {})".format(attempt, attempts)) 373 | try: 374 | sleep_fail = True 375 | time.sleep(1) 376 | sleep_fail = False 377 | except: 378 | pass 379 | if sleep_fail: 380 | raise 381 | else: 382 | print("Blender call failed") 383 | raise 384 | finally: 385 | if p.poll() is None: 386 | print("Killing blender") 387 | if hasattr(os.sys, 'winver'): 388 | os.kill(p.pid, signal.CTRL_BREAK_EVENT) 389 | else: 390 | p.send_signal(signal.SIGKILL) 391 | p.wait() 392 | print("Blender killed") 393 | 394 | if background and self.render_samples > 0: 395 | if os.path.exists(path): 396 | os.remove(path) 397 | shutil.move(blender_outfile.name, path) 398 | print(("Moved image to {}".format(path))) 399 | 400 | except: 401 | with open(blender_err_file_name) as blender_err_file: 402 | print(blender_err_file.read()) 403 | 404 | raise 405 | 406 | finally: 407 | if os.path.exists(blender_outfile.name): 408 | os.remove(blender_outfile.name) 409 | finally: 410 | # Sleep for a short time to let file handles get free'd 411 | time.sleep(0.1) 412 | os.remove(blender_err_file.name) 413 | os.remove(blender_script_file.name) 414 | -------------------------------------------------------------------------------- /eyemodel/blender_script.py.template: -------------------------------------------------------------------------------- 1 | # DO NOT TRY TO RUN THIS FILE, IT ONLY RUNS IN BLENDER 2 | 3 | import bpy 4 | import math 5 | import mathutils 6 | import os 7 | import sys 8 | import functools 9 | import collections 10 | import random 11 | from mathutils import Vector, Matrix 12 | try: 13 | import numpy as np 14 | except: 15 | np = None 16 | 17 | Light = collections.namedtuple("Light", ["location", "target", "type", "size", "strength", "view_angle"]) 18 | 19 | $INPUTS 20 | 21 | scene = bpy.context.scene 22 | armature = bpy.data.objects['Armature Head'] 23 | camera_obj = bpy.data.objects['Camera'] 24 | camera = bpy.data.cameras['Camera'] 25 | 26 | eyeL = bpy.data.objects['eye.L'] 27 | 28 | eyeLbone = armature.pose.bones['def_eye.L'] 29 | pupilLbone = armature.pose.bones['eyepulpex.L'] 30 | eyeLblinkbone = armature.pose.bones['eyeblink.L'] 31 | eyeRbone = armature.pose.bones['def_eye.R'] 32 | pupilRbone = armature.pose.bones['eyepulpex.R'] 33 | eyeRblinkbone = armature.pose.bones['eyeblink.R'] 34 | 35 | cornea_material = bpy.data.materials['eye cornea'] 36 | 37 | compositing_tree = scene.node_tree 38 | 39 | armature_matrix = armature.matrix_world 40 | 41 | scene.frame_current = 0 42 | scene.frame_start = 0 43 | scene.frame_end = 0 44 | 45 | # Get world eye radius and centre 46 | # All other locations are translated/scaled to be relative to these 47 | eye_radius = eyeLbone.bone.length 48 | eye_centre = armature_matrix @ eyeLbone.head 49 | 50 | def input_to_world(val): 51 | scale = eye_radius / input_eye_radius 52 | if isinstance(val, Vector): 53 | return (val - input_eye_pos) * scale + eye_centre 54 | else: 55 | return val * scale 56 | 57 | def world_to_input(val): 58 | scale = eye_radius / input_eye_radius 59 | if isinstance(val, Vector): 60 | return (val - eye_centre) / scale + input_eye_pos 61 | else: 62 | return val / scale 63 | 64 | def look_at_mat(target, up, pos): 65 | zaxis = (target - pos).normalized() 66 | xaxis = up.cross(zaxis).normalized() 67 | yaxis = zaxis.cross(xaxis).normalized() 68 | return Matrix([xaxis, yaxis, zaxis]).transposed() 69 | 70 | def look_at(obj, target, up, pos, initPoseMat): 71 | if isinstance(obj, bpy.types.PoseBone): 72 | pos = armature_matrix @ obj.head 73 | else: 74 | pos = obj.location 75 | 76 | obj_rot_mat = look_at_mat(target, up, pos) @ initPoseMat 77 | #obj_rot_mat = initPoseMat 78 | 79 | if obj.parent: 80 | P = obj.parent.matrix.decompose()[1].to_matrix() 81 | obj_rot_mat = P @ obj_rot_mat @ P.inverted() 82 | 83 | obj.rotation_mode = 'QUATERNION' 84 | obj.rotation_quaternion = obj_rot_mat.to_quaternion() 85 | 86 | corneaGroup = eyeL.vertex_groups['eyecornea.L'] 87 | corneaVertices = [v for v in eyeL.data.vertices if corneaGroup.index in [g.group for g in v.groups]] 88 | 89 | def fitsphere(points): 90 | n = len(points) 91 | points = np.asarray(points) 92 | centroid = np.mean(points, axis=0) 93 | 94 | points = points - centroid 95 | sqmag = [[sum(x*x for x in p)] for p in points] 96 | 97 | Z = np.bmat([sqmag, points, np.ones((n, 1))]) 98 | 99 | M = Z.T @ Z / n 100 | 101 | R = np.ravel(np.asarray(M[-1, :])) 102 | N = np.eye(len(R), dtype=points.dtype) 103 | N[:, 0] = 4 * R 104 | N[0, :] = 4 * R 105 | N[0, 0] = 8 * R[0] 106 | N[-1, 0] = 2 107 | N[0, -1] = 2 108 | N[-1, -1] = 0 109 | 110 | # M A = mu N A = N mu A 111 | # inv(N)M A = mu A 112 | mu, E = np.linalg.eig(np.linalg.inv(N) @ M) 113 | idx = np.argsort(mu) 114 | A = E[:, idx[1]] 115 | 116 | A = np.ravel(np.asarray(A)) 117 | 118 | return (-A[1:-1] / A[0] / 2 + centroid, 119 | math.sqrt((sum(a*a for a in A[1:-1]) - 4*A[0]*A[-1])) / abs(A[0]) / 2) 120 | 121 | if np: 122 | eye_cornea_centre, eye_cornea_radius = fitsphere([tuple(v.co) for v in corneaVertices]) 123 | eye_cornea_centre = Vector(eye_cornea_centre) 124 | cornea_centre = eyeL.matrix_local @ eye_cornea_centre 125 | cornea_radius = (eyeL.matrix_local @ (eye_cornea_centre + Vector((eye_cornea_radius, 0, 0))) - cornea_centre).length 126 | else: 127 | # If the user doesn't have numpy, don't dynamically calculate cornea params, just use hardcoded 128 | cornea_centre = input_to_world(Vector([-0.016783643513917923, -5.577363014221191, 0.04836755618453026])) 129 | cornea_radius = input_to_world(7.922206814944914) 130 | print("WARNING: Blender's python installation doesn't have numpy, using hardcoded cornea parameters") 131 | 132 | for obj in list(bpy.data.objects): 133 | if obj.type == 'LIGHT': 134 | # Ensure the object is in the collection 135 | if obj.name in scene.collection.objects: 136 | scene.collection.objects.unlink(obj) 137 | bpy.data.objects.remove(obj) 138 | 139 | # Add lights 140 | for i, light in enumerate(input_lights): 141 | lamp_type = light.type.upper() 142 | if lamp_type not in ["SPOT", "SUN"]: 143 | raise Error("Light type must be 'sun' or 'spot'") 144 | 145 | # Create new lamp datablock 146 | lamp_data = bpy.data.lights.new(name="Light.{}".format(i), type=lamp_type) 147 | 148 | # Create new object with our lamp datablock 149 | lamp_object = bpy.data.objects.new(name="Light.{}".format(i), object_data=lamp_data) 150 | 151 | # Link lamp object to the scene collection so it'll appear in this scene 152 | bpy.context.scene.collection.objects.link(lamp_object) 153 | 154 | lamp_data.use_nodes = True 155 | 156 | lamp_object.location = input_to_world(light.location) 157 | look_at(lamp_object, 158 | input_to_world(light.target), 159 | input_to_world(input_cam_up) - input_to_world(Vector([0, 0, 0])), 160 | light.location, 161 | Matrix([[-1, 0, 0], 162 | [0, 1, 0], 163 | [0, 0, -1]])) 164 | 165 | if lamp_type == "SPOT": 166 | lamp_data.shadow_soft_size = input_to_world(light.size) 167 | lamp_data.spot_size = light.view_angle * math.pi / 180 168 | lamp_data.spot_blend = 1.0 169 | lamp_data.show_cone = True 170 | 171 | if lamp_type == "SUN": 172 | lamp_data.shadow_soft_size = 1.0 173 | 174 | lamp_data.node_tree.nodes['Emission'].inputs['Strength'].default_value = light.strength 175 | lamp_data.cycles.use_multiple_importance_sampling = True 176 | 177 | # Remove eye IK constraint 178 | for c in list(eyeLbone.constraints.values()): 179 | eyeLbone.constraints.remove(c) 180 | for c in list(eyeRbone.constraints.values()): 181 | eyeRbone.constraints.remove(c) 182 | 183 | # Set eye target 184 | look_at(eyeLbone, 185 | input_to_world(input_eye_target), 186 | (input_to_world(input_eye_up) - input_to_world(Vector([0, 0, 0]))), 187 | eye_centre, 188 | Matrix([[1, 0, 0], 189 | [0, 0, 1], 190 | [0, -1, 0]])) 191 | look_at(eyeRbone, 192 | input_to_world(input_eye_target), 193 | (input_to_world(input_eye_up) - input_to_world(Vector([0, 0, 0]))), 194 | eye_centre, 195 | Matrix([[1, 0, 0], 196 | [0, 0, 1], 197 | [0, -1, 0]])) 198 | 199 | # Set eye blink location 200 | eyeLblinkbone.location[2] = input_eye_closedness * eyeLblinkbone.constraints['Limit Location'].max_z 201 | eyeRblinkbone.location[2] = input_eye_closedness * eyeRblinkbone.constraints['Limit Location'].max_z 202 | 203 | # Calculate base pupil diameter by finding mean distance of pupil vertex to pupil vertex centroid 204 | pupilGroup = eyeL.vertex_groups['eyepulpex.L'] 205 | pupilVertices = [eyeL.matrix_local @ v.co for v in eyeL.data.vertices if pupilGroup.index in [g.group for g in v.groups]] 206 | pupilVertexCentre = functools.reduce(lambda x, y: x + y, pupilVertices) / len(pupilVertices) 207 | pupil_base_radius = sum((v - pupilVertexCentre).length for v in pupilVertices) / len(pupilVertices) 208 | # Set pupil scale 209 | pupil_scale = input_to_world(input_pupil_radius) / pupil_base_radius 210 | if pupil_scale < pupilLbone.constraints["Limit Scaling"].min_x: 211 | raise RuntimeError("Pupil size {} is too small. Minimum size is {}".format( 212 | input_pupil_radius, 213 | world_to_input(pupilLbone.constraints["Limit Scaling"].min_x * pupil_base_radius))) 214 | if pupil_scale > pupilLbone.constraints["Limit Scaling"].max_x: 215 | raise RuntimeError("Pupil size {} is too large. Maximum size is {}".format( 216 | input_pupil_radius, 217 | world_to_input(pupilLbone.constraints["Limit Scaling"].max_x * pupil_base_radius))) 218 | pupilLbone.scale = Vector([pupil_scale, 1, pupil_scale]) 219 | pupilRbone.scale = Vector([pupil_scale, 1, pupil_scale]) 220 | 221 | # Calculate iris radius by finding mean distance of iris vertex to iris vertex centroid 222 | irisGroup = eyeL.vertex_groups['eyeiris.L'] 223 | irisVertices = [eyeL.matrix_local @ v.co for v in eyeL.data.vertices if irisGroup.index in [g.group for g in v.groups]] 224 | irisVertexCentre = functools.reduce(lambda x, y: x + y, irisVertices) / len(irisVertices) 225 | iris_radius = sum((v - irisVertexCentre).length for v in irisVertices) / len(irisVertices) 226 | 227 | bpy.data.images['eye texture'].filepath = '//textures\\ireye-{}.png'.format(input_iris) 228 | 229 | cornea_material.node_tree.nodes['TrickyGlass'].inputs[ 230 | cornea_material.node_tree.nodes['TrickyGlass'].inputs.find('IOR') # workaround for blender bug 231 | ].default_value = input_eye_cornea_refrative_index 232 | 233 | # Set camera location 234 | camera_obj.location = input_to_world(input_cam_pos) 235 | # Set camera target 236 | look_at(camera_obj, 237 | input_to_world(input_cam_target), 238 | input_to_world(input_cam_up) - input_to_world(Vector([0, 0, 0])), 239 | camera_obj.location, 240 | Matrix([[-1, 0, 0], 241 | [0, 1, 0], 242 | [0, 0, -1]])) 243 | 244 | scene.render.resolution_x = input_cam_image_size[0] 245 | scene.render.resolution_y = input_cam_image_size[1] 246 | 247 | camera.sensor_width = 35.0 248 | camera.sensor_height = camera.sensor_width * input_cam_image_size[1] / input_cam_image_size[0] 249 | camera.lens_unit = 'MILLIMETERS' 250 | camera.lens = camera.sensor_width * input_cam_focal_length / input_cam_image_size[0] 251 | camera.dof.focus_distance = input_to_world(input_cam_focus_distance) 252 | 253 | # Set Cycles render settings 254 | scene.cycles.aperture_fstop = input_cam_fstop 255 | scene.cycles.samples = input_render_samples 256 | scene.cycles.seed = input_render_seed % 2147483647 # Max int value for seed 257 | 258 | cam_rand = random.Random() 259 | cam_rand.seed(input_camera_noise_seed) 260 | compositing_tree.nodes["Line Noise"].inputs[0].default_value[0] = cam_rand.uniform(0, 1) 261 | compositing_tree.nodes["Line Noise"].inputs[0].default_value[1] = cam_rand.uniform(0, 1) 262 | compositing_tree.nodes["Shot Noise"].inputs[0].default_value[0] = cam_rand.uniform(0, 1) 263 | compositing_tree.nodes["Shot Noise"].inputs[0].default_value[1] = cam_rand.uniform(0, 1) 264 | 265 | bpy.context.view_layer.update() 266 | 267 | # Use CUDA if possible 268 | if input_use_cuda and bpy.context.preferences.addons['cycles'].preferences.compute_device_type != 'CUDA': 269 | try: 270 | print("Trying to set compute device to CUDA") 271 | bpy.context.preferences.addons['cycles'].preferences.compute_device_type = 'CUDA' 272 | except: 273 | print("Warning: Setting compute device to CUDA failed, using CPU rendering (will be much slower)") 274 | 275 | if output_params_path: 276 | def mlabVec(vec): 277 | return "[{}]".format(", ".join(str(x) for x in vec)) 278 | def mlabMat(mat): 279 | return "[{}]".format(";".join(mlabVec(x) for x in mat)) 280 | 281 | # Get actual pupil vertex positions by applying modifiers to eye mesh 282 | eyeL_mesh = eyeL.evaluated_get(bpy.context.evaluated_depsgraph_get()).to_mesh() 283 | def isPupil(vertex): 284 | for g in vertex.groups: 285 | if g.group == pupilGroup.index and g.weight == 1.0: 286 | return True 287 | 288 | pupilRealVertices = [eyeL.matrix_local @ v.co for v in eyeL_mesh.vertices if isPupil(v)] 289 | 290 | with open(output_params_path, "w") as params_file: 291 | lines = [ 292 | "eye_pos = {};".format(mlabVec(input_eye_pos)), 293 | "eye_target = {};".format(mlabVec(input_eye_target)), 294 | "eye_radius = {};".format(input_eye_radius), 295 | "cornea_centre = {};".format(mlabVec(world_to_input(cornea_centre))), 296 | "cornea_radius = {};".format(world_to_input(cornea_radius)), 297 | "iris_radius = {};".format(world_to_input(iris_radius)), 298 | "pupil_centre = {};".format(mlabVec(world_to_input(armature_matrix @ eyeLbone.tail))), 299 | "pupil_radius = {};".format(input_pupil_radius), 300 | "", 301 | "eye_matrix = {};".format(mlabMat(look_at_mat(pos=input_eye_pos, 302 | target=input_eye_target, 303 | up=input_eye_up) @ Matrix([[-1, 0, 0],[ 0, 1, 0],[ 0, 0,-1]]))), 304 | "eye = eye_make(cornea_radius, eye_matrix);", 305 | "eye.pos_cornea = [cornea_centre(1) -cornea_centre(3) cornea_centre(2) 1]';", 306 | "eye.depth_cornea = cornea_radius - sqrt(cornea_radius^2 - iris_radius^2);", 307 | "eye.n_cornea = {};".format(input_eye_cornea_refrative_index), 308 | "eye.pos_apex = eye.pos_cornea + [0 0 -cornea_radius 0]';", 309 | "eye.pos_pupil = [eye_matrix @ pupil_centre' ; 1];", 310 | "eye.across_pupil = [pupil_radius 0 0 0]';", 311 | "eye.up_pupil = [0 pupil_radius 0 0]';", 312 | "", 313 | "camera_pos = {};".format(mlabVec(input_cam_pos)), 314 | "camera_target = {};".format(mlabVec(input_cam_target)), 315 | "", 316 | "camera_matrix = {};".format(mlabMat(look_at_mat(pos=input_cam_pos, 317 | target=input_cam_target, 318 | up=input_cam_up) @ Matrix([[-1, 0, 0],[ 0, 1, 0],[ 0, 0,-1]]))), 319 | "camera = camera_make();", 320 | "camera.trans = [camera_matrix camera_pos' ; 0 0 0 1];", 321 | "camera.rest_trans = camera.trans;", 322 | "camera.focal_length = {};".format(input_cam_focal_length), 323 | "camera.resolution = [{} {}];".format(*input_cam_image_size), 324 | "", 325 | ] 326 | for i, light in enumerate(input_lights): 327 | lines += [ 328 | "light{} = light_make();".format(i), 329 | "light{}.pos = [{} 1]';".format(i, mlabVec(light.location)), 330 | "cr{i} = eye_find_cr(eye, light{i}, camera);".format(i=i), 331 | "", 332 | ] 333 | lines += [ 334 | "pupil_vertices = [ " + ";".join(" ".join(str(x) for x in world_to_input(v)) + " 1" for v in pupilRealVertices) + "]';" 335 | "", 336 | ] 337 | lines += [ 338 | "figure(1)", 339 | "clf;", 340 | "hold on;", 341 | "eye_draw(eye);", 342 | "plot3([eye_pos(1)], [eye_pos(2)], [eye_pos(3)], 'bx');", 343 | "trans_cornea_centre = eye_matrix @ eye.pos_cornea(1:3);", 344 | "plot3([trans_cornea_centre(1)], [trans_cornea_centre(2)], [trans_cornea_centre(3)], 'gx');", 345 | "trans_pupil_centre = eye_matrix @ eye.pos_pupil(1:3);", 346 | "plot3([trans_pupil_centre(1)], [trans_pupil_centre(2)], [trans_pupil_centre(3)], 'rx');", 347 | "pupil_circle = eye_get_pupil(eye);", 348 | "pupil_circle = [pupil_circle pupil_circle[:,1]];", 349 | "plot3(pupil_circle(1,:), pupil_circle(2,:), pupil_circle(3,:), 'm-');", 350 | "eye_target_short = 2 * eye_radius * eye_target / norm(eye_target);", 351 | "plot3([eye_pos(1) eye_target_short(1)], [eye_pos(2) eye_target_short(2)], [eye_pos(3) eye_target_short(3)], 'b--');", 352 | "", 353 | "plot3(camera.trans(1, 4), camera.trans(2, 4), camera.trans(3, 4), 'k*');", 354 | "", 355 | "up = [0 5 0 1]';", 356 | "across = [10 0 0 1]';", 357 | "in = [0 0 -5 1]';", 358 | "trans_up = camera.trans @ up;", 359 | "trans_across = camera.trans @ across;", 360 | "trans_in = camera.trans @ in;", 361 | "plot3([camera.trans(1,4) trans_up(1)], [camera.trans(2,4) trans_up(2)], [camera.trans(3,4) trans_up(3)], 'k-');", 362 | "plot3([camera.trans(1,4) trans_across(1)], [camera.trans(2,4) trans_across(2)], [camera.trans(3,4) trans_across(3)], 'k-');", 363 | "plot3([camera.trans(1,4) trans_in(1)], [camera.trans(2,4) trans_in(2)], [camera.trans(3,4) trans_in(3)], 'k:');", 364 | "", 365 | ] 366 | for i, light in enumerate(input_lights): 367 | lines += [ 368 | "light_draw(light{i});".format(i=i), 369 | "if ~isempty(cr{i})".format(i=i), 370 | " plot3(cr{i}(1), cr{i}(2), cr{i}(3), 'xr');".format(i=i), 371 | "end", 372 | ] 373 | lines += [ 374 | "", 375 | "hold off;", 376 | "axis equal;", 377 | "xlim auto;", 378 | "ylim auto;", 379 | "zlim auto;", 380 | "view(45, 2*eye_radius);", 381 | "", 382 | ] 383 | lines += [ 384 | "figure(2);", 385 | "clf;", 386 | "I = imread('{}');".format(output_render_path), 387 | "imshow(I);", 388 | "hold on;", 389 | "pupil_refracted_vertices = np.zeros((4, 0));", 390 | "for i in range(1, pupil_vertices.shape[1]):", 391 | " img = eye_find_refraction(eye, camera.trans[:,4], pupil_vertices[:,i]);", 392 | " if img is not None:", 393 | " pupil_refracted_vertices = np.hstack((pupil_refracted_vertices, img));", 394 | "pupil_points = camera_project(camera, pupil_refracted_vertices);", 395 | "pupil_points = pupil_points[:, scipy.spatial.ConvexHull(pupil_points[:2, :].T).vertices];", 396 | "plot(camera.resolution[0]/2 + pupil_points[0, :], camera.resolution[1]/2 - pupil_points[1, :], '-m', linewidth=1.5);", 397 | ] 398 | for i, light in enumerate(input_lights): 399 | lines += [ 400 | "if cr{i} is not None:".format(i=i), 401 | " pcr = camera_project(camera, cr{i});".format(i=i), 402 | " plot(camera.resolution[0]/2 + pcr[0], camera.resolution[1]/2 - pcr[1], 'xr', linewidth=1.5, markersize=15);".format(i=i), 403 | ] 404 | lines += [ 405 | "hold off;" 406 | ] 407 | params_file.write("\n".join(lines)) 408 | #params_file.write("pupil centre = {}\n".format(strVec(world_to_input(armature_matrix @ eyeLbone.tail)))) 409 | # " centre = {}\n".format(strVec(world_to_input(eye_centre)))) 410 | #params_file.write("eye centre = {}\n".format(strVec(world_to_input(eye_centre)))) 411 | #params_file.write("eye radius = {}\n".format(world_to_input(eye_radius))) 412 | #params_file.write("eye target = {}\n".format(strVec(input_eye_target))) 413 | #params_file.write("eye up = {}\n".format(strVec(input_eye_up))) 414 | #params_file.write("eye closedness = {}\n".format(input_eye_closedness)) 415 | #params_file.write("cornea centre = {}\n".format(strVec(world_to_input(cornea_centre)))) 416 | #params_file.write("cornea radius = {}\n".format(world_to_input(cornea_radius))) 417 | #params_file.write("cornea refractive index = {}\n".format(input_eye_cornea_refrative_index)) 418 | #params_file.write("pupil centre = {}\n".format(strVec(world_to_input(armature_matrix @ eyeLbone.tail)))) 419 | #params_file.write("pupil radius = {}\n".format(world_to_input(pupilLbone.scale.x * pupil_base_radius))) 420 | #params_file.write("iris radius = {}\n".format(world_to_input(iris_base_radius))) 421 | #params_file.write("camera position = {}\n".format(strVec(world_to_input(camera_obj.location)))) 422 | #params_file.write("camera target = {}\n".format(strVec(input_cam_target))) 423 | #params_file.write("camera up = {}\n".format(strVec(input_cam_up))) 424 | #params_file.write("camera focal length = {}\n".format(input_cam_focal_length)) 425 | #params_file.write("camera focus distance = {}\n".format(input_cam_focus_distance)) 426 | #params_file.write("camera fstop = {}\n".format(input_cam_fstop)) 427 | #params_file.write("image size = {}\n".format(tuple(input_cam_image_size))) 428 | # vim: ft=python 429 | --------------------------------------------------------------------------------