|
| 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