-
Notifications
You must be signed in to change notification settings - Fork 53
/
gfxutil.py
executable file
·227 lines (192 loc) · 8.58 KB
/
gfxutil.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
"""Utility functions used by the eyes code, related to 2D SGV paths and
3D pi3d geometry."""
import math
import pi3d
from svg.path import parse_path
def get_view_box(root):
"""Get artboard bounds (to use Illustrator terminology)
from SVG DOM tree:"""
for node in root.childNodes:
if node.nodeType == node.ELEMENT_NODE:
view_box = get_view_box(node)
if view_box:
return view_box
if node.tagName.lower() == "svg":
view_box = node.getAttribute("viewBox").split()
return (float(view_box[0]), float(view_box[1]),
float(view_box[2]), float(view_box[3]))
return None
def get_path(root, path_name):
"""Search for and return a specific path (by name) in SVG DOM tree:"""
for node in root.childNodes:
if node.nodeType == node.ELEMENT_NODE:
path = get_path(node, path_name)
if path:
return path
if((node.tagName.lower() == "path") and
(node.getAttribute("id") == path_name)):
return parse_path(node.getAttribute("d"))
return None
def path_to_points(path, num_points, closed, reverse):
"""Convert SVG path to a 2D point list. Provide path, number of points,
and whether or not this is a closed path (loop). For closed loops,
the size of the point list returned is one element larger than the
number of points passed, and the first and last elements will
coincide."""
points = []
num_points = max(num_points, 2)
if closed:
div = float(num_points)
else:
div = float(num_points - 1)
for point_num in range(num_points):
if reverse:
point = path.point(1.0 - point_num / div, error=1e-5)
else:
point = path.point(point_num / div, error=1e-5)
points.append((point.real, point.imag))
if closed:
points.append(points[0])
return points
def get_points(root, path_name, num_points, closed, reverse):
"""Combo wrapper for path_to_points(get_path(...))."""
return path_to_points(get_path(root, path_name),
num_points, closed, reverse)
def scale_points(points, view_box, radius):
"""Scale a given 2D point list by normalizing to a given view box
(returned by get_view_box()) then expanding to a given size
centered on (0,0)."""
for point_num, _ in enumerate(points): # Index of each point in path
points[point_num] = (((points[point_num][0] - view_box[0]) /
view_box[2] - 0.5) * radius * 2.0,
((points[point_num][1] - view_box[1]) /
view_box[3] - 0.5) * radius * -2.0)
def points_interp(points1, points2, weight2):
"""Interpolate between two 2D point lists, returning a new point list.
Specify weighting (0.0 to 1.0) of second list. Lists should have
same number of points; if not, lesser point count is used and the
output may be weird."""
num_points = min(len(points1), len(points2))
if num_points < 1:
return None
weight2 = min(max(0.0, weight2), 1.0)
weight1 = 1.0 - weight2
points = []
for point_num in range(num_points):
points.append(
(points1[point_num][0] * weight1 + points2[point_num][0] * weight2,
points1[point_num][1] * weight1 + points2[point_num][1] * weight2))
return points
def points_bounds(points):
"""Return bounding rect of 2D point list (as 4-tuple of min X, min Y,
max X, max Y)."""
min_x, min_y, max_x, max_y = (points[0][0], points[0][1],
points[0][0], points[0][1])
for point in points[1:]:
min_x = min(min_x, point[0])
min_y = min(min_y, point[1])
max_x = max(max_x, point[0])
max_y = max(max_y, point[1])
return (min_x, min_y, max_x, max_y)
def re_axis(shape, texture_offset):
"""Rotates a model 90 degrees on the X axis and applies an offset to
the texture map's U axis. pi3d.Lathe() operates around the Y axis,
but the eyes need symmetry around the Z axis and applying that
transformation along with the eye rotation produced undesirable
motion paths. This is a hacky workaround. It messes around with
some pi3d data structures directly that it probably shouldn't,
and might break with future releases of that code."""
# vertices = buf[0,1,2]
# normals = buf[3,4,5]
# tex_coords = buf[6,7,8]
abuf = shape.buf[0].array_buffer
for vertex_num, _ in enumerate(abuf):
# Rotate vertex
abuf[vertex_num][1], abuf[vertex_num][2] = (
abuf[vertex_num][2], -abuf[vertex_num][1])
# Rotate normal
abuf[vertex_num][4], abuf[vertex_num][5] = (
abuf[vertex_num][5], -abuf[vertex_num][4])
# Offset texture map on U axis
abuf[vertex_num][6] += texture_offset
# Instead of making these so general-purpose, I might intentionally
# rig them to specifically handle the iris (closed shape) and eyelid
# (open shape) cases. Esp. since the iris is a weird case that'll need
# 3 paths (edge, open and closed)
def mesh_init(uv_steps, uv_offset, closed, lid):
"""Given number of U and V steps, generate boilerplate object with
given texture coordinates and point indices. The initial vertex
positions can be dummies (all 0,0,0) and will be reassigned later.
If it's an eyelid, add an extra row with V=0.0."""
# U steps might be determined by passing in a pointlist instead,
# even though we're not using the coordinates yet, it'd provide some
# consistency and avoid trouble later.
verts = []
tex = []
idx = []
norms = []
if closed:
uv_steps = (uv_steps[0] + 1, uv_steps[1])
uv_div = (float(uv_steps[0] - 1), float(uv_steps[1] - 1))
if lid: # Add extra row of vertices (with V=0) if eyelid
for u_pos in range(uv_steps[0]):
verts.append((0, 0, 0))
tex.append((u_pos / uv_div[0] + uv_offset[0], uv_offset[1]))
norms.append((0, 0, -1))
v_range = uv_steps[1]
else:
v_range = uv_steps[1] - 1
for v_pos in range(uv_steps[1]):
v_pos_2 = (uv_offset[1] +
(v_pos / uv_div[1]) * (1.0 - uv_offset[1] * 2.0))
for u_pos in range(uv_steps[0]):
verts.append((0, 0, 0))
tex.append((u_pos / uv_div[0] + uv_offset[0], v_pos_2))
norms.append((0, 0, -1))
for v_pos in range(v_range):
for u_pos in range(uv_steps[0] - 1):
pos = v_pos * uv_steps[0] + u_pos
idx.append((pos + uv_steps[0], pos, pos + 1))
idx.append((pos + 1, pos + uv_steps[0] + 1, pos + uv_steps[0]))
shape = pi3d.Shape(None, None, "foo", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1.0, 1.0, 1.0, 0.0, 0.0, 0.0)
shape.buf = [pi3d.Buffer(shape, verts, tex, idx, norms, False)]
return shape
def points_mesh(points, steps, z_coord, flip=False):
"""Generate mesh between two point lists. U axis steps are determined
by number of points, V axis determined by 'steps'"""
steps = max(steps, 2)
num_points = min(len(points[1]), len(points[2]))
if num_points < 1:
return None
verts = []
if flip is True:
if points[0]:
for point in reversed(points[0]):
verts.append((-point[0], point[1], z_coord))
div = float(steps - 1)
for y_index in range(steps):
point_list = points_interp(points[1], points[2], y_index / div)
for point in reversed(point_list):
verts.append((-point[0], point[1], z_coord))
else:
if points[0]:
for point in points[0]:
verts.append((point[0], point[1], z_coord))
div = float(steps - 1)
for y_index in range(steps):
point_list = points_interp(points[1], points[2], y_index / div)
for point in point_list:
verts.append((point[0], point[1], z_coord))
return verts
def zangle(points, eye_radius):
"""Determines the Z depth and angle-from-Z axis of an SVG feature
(ostensibly a circle, polygonalized by get_points()); for example,
the depth of the iris, or the start and end angles for the curve
that's lathed to form the sclera. Pass point list and eye radius."""
xdist = points[0][0]
ydist = points[0][1]
radius = math.sqrt(xdist * xdist + ydist * ydist) # R of SVG feature
z_depth = math.sqrt(eye_radius * eye_radius - radius * radius)
angle = math.atan2(radius, eye_radius) * 180.0 / math.pi
return (z_depth, angle)