Skip to content

Commit 04d0b9b

Browse files
committed
Simple cylinder geometry from a bezier
1 parent 9826914 commit 04d0b9b

File tree

1 file changed

+194
-0
lines changed

1 file changed

+194
-0
lines changed

make_branch_geometry.py

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
#!/usr/bin/env python3
2+
3+
import numpy as np
4+
from json import load
5+
6+
class MakeTreeGeometry:
7+
_profiles = None
8+
_type_names = {"trunk", "sidebranch", "branch"}
9+
10+
def __init__(self, data_dir):
11+
""" Read in profiles, if not already read in"""
12+
13+
# Read in global parameters
14+
MakeTreeGeometry._read_profiles(data_dir)
15+
16+
# Set per-branch/trunk params
17+
self.n_along = 10
18+
self.n_around = 64
19+
20+
# Information about the current branch/trunk
21+
self.pt1 = [-0.5, 0.0, 0.0]
22+
self.pt2 = [0.0, 0.0, 0.0]
23+
self.pt3 = [0.5, 0.0, 0.0]
24+
self.start_radii = 0.5
25+
self.end_radii = 0.25
26+
self.start_is_junction = True
27+
28+
self.vertex_locs = np.zeros((self.n_along, self.n_around, 3))
29+
30+
31+
@staticmethod
32+
def _read_profiles(data_dir):
33+
""" Read in all the profile curves for the various branch types"""
34+
if MakeTreeGeometry._profiles is not None:
35+
return
36+
37+
MakeTreeGeometry._profiles = {}
38+
for t in MakeTreeGeometry._type_names:
39+
try:
40+
fname = data_dir + "/" + t + "_profiles.json"
41+
with open(fname, "r") as fp:
42+
MakeTreeGeometry._profiles[t] = load(fp)
43+
except:
44+
pass
45+
46+
def n_vertices(self):
47+
return self.n_along * self.n_around
48+
49+
def set_dims(self, n_along=10, n_radial=64):
50+
self.n_along = n_along
51+
self.n_around = n_radial
52+
self.vertex_locs = np.zeros((self.n_along, self.n_around, 3))
53+
54+
def set_pts(self, pt1, pt2, pt3):
55+
""" Turn into numpy array
56+
@param pt1 First point
57+
@param pt2 Mid point
58+
@param pt3 End point
59+
"""
60+
self.pt1 = pt1
61+
self.pt2 = pt2
62+
self.pt3 = pt3
63+
64+
def set_pts_from_pt_tangent(self, pt1, vec1, pt3):
65+
"""Set the points from a starting point/tangent
66+
@param pt1 - starting point
67+
@param vec1 - starting tangent
68+
@param pt3 - ending point"""
69+
# v = - 2 * p0 + 2 * p1
70+
# v/2 + p2 = p1
71+
mid_pt = np.array(pt1) + np.array(vec1) * 0.5
72+
self.set_pts(pt1, mid_pt, pt3)
73+
74+
def set_radii(self, start_radius=1.0, end_radius=1.0, b_start_is_junction=False):
75+
""" Set the radius of the branch
76+
@param start_radius - radius at pt1
77+
@param end_radius - radius at pt3
78+
@param b_start_is_junction - is the start of the curve a junction? """
79+
self.start_radii = start_radius
80+
self.end_radii = end_radius
81+
self.start_is_junction = b_start_is_junction
82+
83+
def pt_axis(self, t):
84+
""" Return a point along the bezier
85+
@param t in 0, 1
86+
@return 2 or 3d point"""
87+
pts_axis = np.array([self.pt1[i] * (1-t) ** 2 + 2 * (1-t) * t * self.pt2[i] + t ** 2 * self.pt3[i] for i in range(0, 3)])
88+
return pts_axis.transpose()
89+
#return self.p0 * (1-t) ** 2 + 2 * (1-t) * t * self.p1 + t ** 2 * self.p2
90+
91+
def tangent_axis(self, t):
92+
""" Return the tangent vec
93+
@param t in 0, 1
94+
@return 3d vec"""
95+
vec_axis = [2 * t * (self.pt1[i] - 2.0 * self.pt2[i] + self.pt3[i]) - 2 * self.pt1[i] + 2 * self.pt2[i] for i in range(0, 3)]
96+
return np.array(vec_axis)
97+
98+
def binormal_axis(self, t):
99+
""" Return the bi-normal vec, cross product of first and second derivative
100+
@param t in 0, 1
101+
@return 3d vec"""
102+
vec_tang = self.tangent_axis(t)
103+
vec_tang = vec_tang / np.linalg.norm(vec_tang)
104+
vec_second_deriv = np.array([2 * (self.pt1[i] - 2.0 * self.pt2[i] + self.pt3[i]) for i in range(0, 3)])
105+
106+
vec_binormal = np.cross(vec_tang, vec_second_deriv)
107+
if np.isclose(np.linalg.norm(vec_second_deriv), 0.0):
108+
for i in range(0, 2):
109+
if not np.isclose(vec_tang[i], 0.0):
110+
vec_binormal[i] = -vec_tang[(i+1)%3]
111+
vec_binormal[(i+1)%3] = vec_tang[i]
112+
vec_binormal[(i+2)%3] = 0.0
113+
break
114+
115+
return vec_binormal / np.linalg.norm(vec_binormal)
116+
117+
def frenet_frame(self, t):
118+
""" Return the matrix that will take the point 0,0,0 to crv(t) with x axis along tangent, y along binormal
119+
@param t - t value
120+
@return 4x4 transformation matrix"""
121+
pt_center = self.pt_axis(t)
122+
vec_tang = self.tangent_axis(t)
123+
vec_tang = vec_tang / np.linalg.norm(vec_tang)
124+
vec_binormal = self.binormal_axis(t)
125+
vec_x = np.cross(vec_tang, vec_binormal)
126+
127+
mat = np.identity(4)
128+
mat[0:3, 3] = -pt_center[0:3]
129+
mat[0:3, 0] = vec_x.transpose()
130+
mat[0:3, 1] = vec_binormal.transpose()
131+
mat[0:3, 2] = vec_tang.transpose()
132+
133+
return mat
134+
135+
def _calc_cyl_vertices(self):
136+
"""Calculate the cylinder vertices"""
137+
pt = np.ones((4))
138+
radii = np.linspace(self.start_radii, self.end_radii, self.n_along)
139+
if self.start_is_junction:
140+
radii_exp = self.start_radii * 0.25 * np.exp(np.linspace(0, -10.0, self.n_along))
141+
radii = radii + radii_exp
142+
143+
for it, t in enumerate(np.linspace(0, 1.0, self.n_along)):
144+
mat = self.frenet_frame(t)
145+
pt[0] = 0
146+
pt[1] = 0
147+
pt[2] = 0
148+
pt_on_crv = mat @ pt
149+
for itheta, theta in enumerate(np.linspace(0, np.pi * 2.0, self.n_around, endpoint=False)):
150+
pt[0] = np.cos(theta) * radii[it]
151+
pt[1] = np.sin(theta) * radii[it]
152+
pt[2] = 0
153+
pt_on_crv = mat @ pt
154+
155+
self.vertex_locs[it, itheta, :] = pt_on_crv[0:3].transpose()
156+
157+
def write_mesh(self, fname):
158+
"""Write out an obj file with the appropriate geometry
159+
@param fname - file name (should end in .obj"""
160+
with open(fname, "w") as fp:
161+
fp.write(f"# Branch\n")
162+
for it in range(0, self.n_along):
163+
for ir in range(0, self.n_around):
164+
fp.write(f"v ")
165+
fp.write(" ".join(["{:.6}"] * 3).format(*self.vertex_locs[it, ir, :]))
166+
fp.write(f"\n")
167+
for it in range(0, self.n_along - 1):
168+
i_curr = it * self.n_around + 1
169+
i_next = (it+1) * self.n_around + 1
170+
for ir in range(0, self.n_around):
171+
ir_next = (ir + 1) % self.n_around
172+
fp.write(f"f {i_curr + ir} {i_curr + ir_next} {i_next + ir_next} \n")
173+
fp.write(f"f {i_curr + ir} {i_next + ir_next} {i_next + ir} \n")
174+
175+
def _make_cyl(self, radius_start, radius_end, start_is_junction, profiles):
176+
""" Make a 3D generalized cylinder"""
177+
self.set_radii(start_radius=radius_start, end_radius=radius_end, b_start_is_junction=start_is_junction)
178+
self._calc_cyl_vertices()
179+
180+
def make_branch_segment(self, pt1, pt2, pt3, radius_start, radius_end, start_is_junction):
181+
""" Output a 3D generalized cylinder"""
182+
self.set_pts(pt1, pt2, pt3)
183+
try:
184+
self._make_cyl(radius_start, radius_end, start_is_junction, MakeTreeGeometry._profiles["sidebranches"])
185+
except KeyError:
186+
self._make_cyl(radius_start, radius_end, start_is_junction, None)
187+
188+
189+
if __name__ == '__main__':
190+
branch = MakeTreeGeometry("data")
191+
branch.make_branch_segment([-0.5, 0.0, 0.0], [0.0, 0.1, 0.05], [0.5, 0.0, 0.0], 0.5, 0.25, True)
192+
branch.write_mesh("data/cyl.obj")
193+
194+

0 commit comments

Comments
 (0)