├── .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 |
--------------------------------------------------------------------------------