def sample_p(self, pnt: 'geo.Point', u1: FLOAT, u2: FLOAT) -> ['geo.Point', 'geo.Normal']: """ uniformly sample the sphere visible (of certain solid angle) to the point """ # compute coords for sampling ctr = self.o2w(geo.Point(0., 0., 0.)) wc = geo.normalize(ctr - pnt) _, wc_x, wc_y = geo.coordinate_system(wc) # sample uniformly if p is inside if pnt.sq_dist(ctr) - self.radius * self.radius < EPS: return self.sample(u1, u2) # sample inside subtended cone st_max_sq = self.radius * self.radius / pnt.sq_dist(ctr) ct_max = np.sqrt(max(0., 1. - st_max_sq)) from pytracer.montecarlo import uniform_sample_cone r = geo.Ray(pnt, uniform_sample_cone(u1, u2, ct_max, wc_x, wc_y, wc), EPS) hit, thit, _, _ = self.intersect(r) if not hit: thit = (ctr - pnt).dot(geo.normalize(r.d)) ps = r(thit) ns = geo.Normal.from_arr(geo.normalize(ps - ctr)) if self.ro: ns *= -1. return [ps, ns]
def generate_ray(self, sample: 'CameraSample') -> [FLOAT, 'geo.Ray']: """ Generate ray based on image sample. Returned ray direction is normalized """ # generate raster and camera samples Pras = geo.Point(sample.imageX, sample.imageY, 0.) Pcam = self.r2c(Pras) ray = geo.Ray(geo.Point(0., 0., 0.), geo.Vector.from_arr(geo.normalize(Pcam)), 0., np.inf) # ray.d is a geo.Vector init from a geo.Point # modify ray for dof if self.lens_rad > 0.: # sample point on lens from pytracer.montecarlo import concentric_sample_disk lens_u, lens_v = \ concentric_sample_disk(sample.lens_u, sample.lens_v) lens_u *= self.lens_rad lens_v *= self.lens_rad # compute point on focal plane ft = self.focal_dist / ray.d.z Pfoc = ray(ft) # update ray ray.o = geo.Point(lens_u, lens_v, 0.) ray.d = geo.normalize(Pfoc - ray.o) ray.time = util.lerp(sample.time, self.s_open, self.s_close) ray = self.c2w(ray) return [1., ray]
def look_at(cls, pos: 'geo.Point', look: 'geo.Point', up: 'geo.Vector', dtype=FLOAT) -> 'Transform': """ look_at Look-at transformation, from camera to world """ w2c = np.eye(4, 4, dtype=dtype) c2w = np.eye(4, 4, dtype=dtype) zc = geo.normalize(look - pos) xc = geo.normalize(geo.normalize(up).cross(zc)) yc = zc.cross(xc) # orthogonality # c2w translation c2w[0][3] = pos.x c2w[1][3] = pos.y c2w[2][3] = pos.z # c2w rotation c2w[0][0] = xc.x c2w[0][1] = xc.y c2w[0][2] = xc.z c2w[1][0] = yc.x c2w[1][1] = yc.y c2w[1][2] = yc.z c2w[2][0] = zc.x c2w[2][1] = zc.y c2w[2][2] = zc.z # w2c rotation # in effect as camera extrinsic w2c[0][0] = xc.x w2c[0][1] = yc.x w2c[0][2] = zc.x w2c[1][0] = xc.y w2c[1][1] = yc.y w2c[1][2] = zc.y w2c[2][0] = xc.z w2c[2][1] = yc.z w2c[2][2] = zc.z # w2c translation w2c[0][3] = -(pos.x * xc.x + pos.y * yc.x + pos.z * zc.x) w2c[1][3] = -(pos.x * xc.y + pos.y * yc.y + pos.z * zc.y) w2c[2][3] = -(pos.x * xc.z + pos.y * yc.z + pos.z * zc.z) return cls(c2w, w2c, dtype)
def sample_l(self, p: 'geo.Point', pEps: FLOAT, ls: 'LightSample', time: FLOAT,) -> ['Spectrum', 'geo.Vector', FLOAT, 'VisibilityTester']: wi = geo.normalize(self.pos - p) pdf = 1. vis = VisibilityTester() vis.set_segment(p, pEps, self.pos, 0., time) return [self.intensity / self.__scale(-wi), wi, pdf, vis]
def __cylinder(self, p: 'geo.Point') -> [FLOAT]: """ Cylinderical Mapping for single point. Returns list [s, t]. """ v = geo.normalize(self.w2t(p) - geo.Point(0., 0., 0.)) return [(PI + self.arctan2(v.y, v.x)) * INV_2PI, v.z]
def sample_l(self, p: 'geo.Point', pEps: FLOAT, ls: 'LightSample', time: FLOAT,) -> ['Spectrum', 'geo.Vector', FLOAT, 'VisibilityTester']: ps, ns = self.shape_set.sample_p(p, ls) wi = geo.normalize(ps - p) pdf = self.shape_set.pdf(p, wi) vis = VisibilityTester() vis.set_segment(p, pEps, ps, EPS, time) return [self.l(ps, ns, -wi), wi, pdf, vis]
def __init__(self, sig_a: 'Spectrum', sig_s: 'Spectrum', g: FLOAT, le: 'Spectrum', extent: 'geo.BBox', v2w: 'trans.Transform', a: FLOAT, b: FLOAT, up: 'geo.Vector'): super().__init__(sig_a, sig_s, g, le, v2w) self.extent = extent self.a = a self.b = b self.up = geo.normalize(up)
def generate_ray_differential(self, sample: 'CameraSample') -> [FLOAT, 'geo.RayDifferential']: """ Generate ray differential. """ p_ras = geo.Point(sample.imageX, sample.imageY, 0.) p_cam = self.r2c(p_ras) ray = geo.RayDifferential(geo.Point(0., 0., 0.), geo.Vector.from_arr(geo.normalize(p_cam)), 0., np.inf) # ray.d is a geo.Vector init from a geo.Point from pytracer.montecarlo import concentric_sample_disk if self.lens_rad > 0.: # depth of field lens_u, lens_v = \ concentric_sample_disk(sample.lens_u, sample.lens_v) lens_u *= self.lens_rad lens_v *= self.lens_rad # compute point on focal plane ft = self.focal_dist / ray.d.z Pfoc = ray(ft) # update ray ray.o = geo.Point(lens_u, lens_v, 0.) ray.d = geo.normalize(Pfoc - ray.o) if self.lens_rad > 0.: # with defocus blue lens_u, lens_v = concentric_sample_disk(sample.lens_u, sample.lens_v) lens_u *= self.lens_rad lens_v *= self.lens_rad # compute point on focal plane dx = geo.normalize(self.dxCam + p_cam) ft = self.focal_dist / dx.z Pfoc = geo.Point(0., 0., 0.) + ft * dx ray.rxOrigin = geo.Point(lens_u, lens_v, 0.) ray.rxDirection = geo.normalize(Pfoc - ray.rxOrigin) dy = geo.normalize(geo.Vector.from_arr(p_cam + self.dyCam)) ft = self.focal_dist / dy.z Pfoc = geo.Point(0., 0., 0.) + ft * dy ray.ryOrigin = geo.Point(lens_u, lens_v, 0.) ray.ryDirection = geo.normalize(Pfoc - ray.ryOrigin) else: ray.rxOrigin = ray.ryOrigin = ray.o ray.rxDirection = geo.normalize(self.dxCam + p_cam) # geo.Vector + geo.Point => geo.Vector ray.ryDirection = geo.normalize(self.dyCam + p_cam) ray.time = sample.time ray = self.c2w(ray) ray.has_differentials = True return [1., ray]
def pdf(self, wo: 'geo.Vector', wi: 'geo.Vector') -> FLOAT: wh = geo.normalize(wo + wi) ct = abs_cos_theta(wh) if wo.dot(wh) <= 0.: return 0. return ( (self.e + 1.) * np.power(ct, self.e)) / (2. * PI * 4. * wo.dot(wh))
def sample(self, u1: FLOAT, u2: FLOAT) -> ['geo.Point', 'geo.Normal']: z = util.lerp(u1, self.zmin, self.zmax) t = u2 * self.phiMax p = geo.Point(self.radius * np.cos(t), self.radius * np.sin(t), z) Ns = geo.normalize(self.o2w(geo.Normal(p.x, p.y, 0.))) if self.ro: Ns *= -1. return [self.o2w(p), Ns]
def __sphere(self, p: 'geo.Point') -> [FLOAT]: """ Spherical Mapping for single point. Returns list [s, t]. """ v = geo.normalize(self.w2t(p) - geo.Point(0., 0., 0.)) theta = geo.spherical_theta(v) phi = geo.spherical_phi(v) return [theta * INV_PI, phi * INV_2PI]
def pdf(self, wo: 'geo.Vector', wi: 'geo.Vector') -> FLOAT: wh = geo.normalize(wo + wi) ct = abs_cos_theta(wh) ds = 1. - ct * ct if ds > 0. and wo.dot(wh) > 0.: return (np.sqrt((self.ex + 1.) * (self.ey + 1.)) * INV_2PI * np.power(ct, ( self.ex * wh.x * wh.x + self.ey * wh.y * wh.y) / ds)) / \ (4. * wo.dot(wh)) else: return 0.
def f(self, wo: 'geo.Vector', wi: 'geo.Vector') -> 'Spectrum': ct_i = cos_theta(wi) ct_o = cos_theta(wo) if ct_i == 0. or ct_o == 0.: return Spectrum(0.) wh = geo.normalize(wi + wo) ct_h = wi.dot(wh) F = self.fresnel(ct_h) return self.R * self.distribution.D(wh) * self.G(wo, wi, wh) * \ F / (4. * ct_i * ct_o)
def f(self, wo: 'geo.Vector', wi: 'geo.Vector') -> 'Spectrum': diffuse = (28. / (23. * PI)) * self.Rd * \ (Spectrum(1.) - self.Rs) * \ (1. - np.power(1. - .5 * abs_cos_theta(wi), 5)) * \ (1. - np.power(1. - .5 * abs_cos_theta(wo), 5)) wh = geo.normalize(wi + wo) specular = self.distribution.D(wh) / \ (4. * wi.abs_dot(wh) * max(abs_cos_theta(wi), abs_cos_theta(wo))) * \ self.schlick(wi.dot(wh)) return diffuse + specular
def sample(self, u1: FLOAT, u2: FLOAT) -> ['geo.Point', 'geo.Normal']: # account for partial disk from pytracer.montecarlo import concentric_sample_disk x, y = concentric_sample_disk(u1, u2) phi = np.arctan2(y, x) * self.phiMax * INV_2PI r = self.inner_radius + np.sqrt(x * x + y * y) * (self.radius - self.inner_radius) p = geo.Point(r * np.cos(phi), r * np.sin(phi), self.height) Ns = geo.normalize(self.o2w(p)) if self.ro: Ns *= -1. return [self.o2w(p), Ns]
def __falloff(self, w: 'geo.Vector') -> FLOAT: """ __falloff() Determines the falloff given a vector in the world. """ wl = geo.normalize(self.w2l(w)) ct = wl.z if ct < self.cos_width: return 0. if ct > self.cos_falloff: return 1. # falloff inside the cone d = (ct - self.cos_width) / (self.cos_falloff - self.cos_width) return d * d * d * d
def sample(self, u1: FLOAT, u2: FLOAT) -> ['geo.Point', 'geo.Normal']: from pytracer.montecarlo import uniform_sample_triangle b1, b2 = uniform_sample_triangle(u1, u2) p = b1 * self.mesh.p[self.mesh.vertexIndex[self.v]] + \ b2 * self.mesh.p[self.mesh.vertexIndex[self.v]+1] + \ (1. - b1 - b2) * self.mesh.p[self.mesh.vertexIndex[self.v+2]] Ns = geo.normalize(geo.Normal.from_arr( (self.mesh.p[self.mesh.vertexIndex[self.v+1]] - self.mesh.p[self.mesh.vertexIndex[self.v]]) \ .cross(self.mesh.p[self.mesh.vertexIndex[self.v+2]] - self.mesh.p[self.mesh.vertexIndex[self.v]]))) if self.ro: Ns *= -1. return [p, Ns]
def __init__(self, dg: 'geo.DifferentialGeometry', ng: 'geo.Normal', e: FLOAT = 1.): """ dg: geo.DifferentialGeometry ng: Geometric geo.Normal e: index of refraction """ self.dgs = dg self.eta = e self.ng = ng # coord. system self.nn = dg.nn self.sn = geo.normalize(dg.dpdu) self.tn = self.nn.cross(self.sn) self.bdfs = [] self.__nBDF = INT(0)
def sample(self, u1: FLOAT, u2: FLOAT) -> ['geo.Point', 'geo.Normal']: """ account for partial sphere """ from pytracer.montecarlo import uniform_sample_sphere v = uniform_sample_sphere(u1, u2) phi = geo.spherical_theta(v) * self.phiMax * INV_2PI theta = self.thetaMin + geo.spherical_theta(v) * (self.thetaMax - self.thetaMin) v = geo.spherical_direction(np.sin(theta), np.cos(theta), phi) * self.radius v.z = self.zmin + v.z * (self.zmax - self.zmin) p = geo.Point.from_arr(v) Ns = geo.normalize(self.o2w(geo.Normal(p.x, p.y, p.z))) if self.ro: Ns *= -1. return [self.o2w(p), Ns]
def rotate(cls, angle, axis: 'geo.Vector', dtype=FLOAT) -> 'Transform': a = geo.normalize(axis) s = np.sin(np.deg2rad(angle)) c = np.cos(np.deg2rad(angle)) m = np.eye(4, 4, dtype=dtype) m[0][0] = a.x * a.x + (1. - a.x * a.x) * c m[0][1] = a.x * a.y * (1. - c) - a.z * s m[0][2] = a.x * a.z * (1. - c) + a.y * s m[1][0] = a.x * a.y * (1. - c) + a.z * s m[1][1] = a.y * a.y + (1. - a.y * a.y) * c m[1][2] = a.y * a.z * (1. - c) - a.x * s m[2][0] = a.x * a.z * (1. - c) - a.y * s m[2][1] = a.y * a.z * (1. - c) + a.x * s m[2][2] = a.z * a.z + (1. - a.z * a.z) * c return cls(m, m.T, dtype)
def __scale(self, w: 'geo.Vector') -> 'Spectrum': """ __scale() Utility method to scale the amount of light projected in the given direction. Assume the scale texture is encoded using spherical coordinates. """ if self.MIPMap is None: return Spectrum(1.) wp = geo.normalize(self.w2l(w)) wp.z, wp.y = wp.y, wp.z theta = geo.spherical_theta(wp) phi = geo.spherical_phi(wp) s = phi * INV_2PI t = theta * INV_PI return Spectrum(self.projMap.look_up([s, t]), SpectrumType.ILLUMINANT)
def le(self, rd: 'geo.RayDifferential') -> 'Spectrum': wh = geo.normalize(self.w2l(rd.d)) s = geo.spherical_phi(wh) * INV_2PI t = geo.spherical_theta(wh) * INV_PI return Spectrum.from_rgb(self.radMap.look_up(s, t), SpectrumType.ILLUMINANT)
def intersect_p(self, r: 'Ray') -> bool: """ Determine whether intersects using Barycentric coordinates """ # compute s1 p1 = self.mesh.p[self.mesh.vertexIndex[self.v]] p2 = self.mesh.p[self.mesh.vertexIndex[self.v + 1]] p3 = self.mesh.p[self.mesh.vertexIndex[self.v + 2]] e1 = p2 - p1 # geo.Vector e2 = p3 - p1 s1 = r.d.cross(e2) div = s1.dot(e1) if div == 0.: return False divInv = 1. / div # compute barycentric coordinate ## first one d = r.o - p1 b1 = d.dot(s1) * divInv if b1 < 0. or b1 > 1.: return False ## second one s2 = d.cross(e1) b2 = r.d.dot(s2) * divInv if b2 < 0. or (b1 + b2) > 1.: return False # compute intersection t = e2.dot(s2) * divInv if t < r.mint or t > r.maxt: return False # compute partial derivatives uvs = self.get_uvs() du1 = uvs[0][0] - uvs[2][0] du2 = uvs[1][0] - uvs[2][0] dv1 = uvs[0][1] - uvs[2][1] dv2 = uvs[1][1] - uvs[2][1] dp1 = p1 - p3 dp2 = p2 - p3 det = du1 * dv2 - du2 * dv1 if det == 0.: # choose an arbitrary system _, dpdu, dpdv = geo.coordinate_system(geo.normalize(e2.cross(e1))) else: detInv = 1. / det dpdu = (dv2 * dp1 - dv1 * dp2) * detInv dpdv = (-du2 * dp1 + du1 * dp2) * detInv # interpolate triangle parametric coord. b0 = 1. - b1 - b2 tu = b0 * uvs[0][0] + b1 * uvs[1][0] + b2 * uvs[2][0] tv = b0 * uvs[0][1] + b1 * uvs[1][1] + b2 * uvs[2][1] # test alpha texture dg = geo.DifferentialGeometry(r(t), dpdu, dpdv, geo.Normal(0., 0., 0.), geo.Normal(0., 0., 0.), tu, tv, self) if self.mesh.alphaTexture is not None: # alpha mask presents if self.mesh.alphaTexture.evaluate(dg) == 0.: return False # have a hit return True
def __init__(self, l2w: 'trans.Transform', radiance: 'Spectrum', di: 'geo.Vector'): super().__init__(l2w) self.di = geo.normalize(l2w(di)) self.l = radiance
def get_shading_geometry( self, o2w: 'trans.Transform', dg: 'geo.DifferentialGeometry') -> 'geo.DifferentialGeometry': if self.mesh.n is None or self.mesh.s is None: return dg # compute barycentric coord uvs = self.get_uvs() A = np.array([[uvs[1][0] - uvs[0][0], uvs[2][0] - uvs[0][0]], [uvs[1][1] - uvs[0][1], uvs[2][1] - uvs[0][1]]], dtype=FLOAT) C = np.array([dg.u - uvs[0][0], dg.v - uvs[0][1]]) try: b = np.linalg.solve(A, C) except: b = [1. / 3, 1. / 3, 1. / 3] else: b = [1. - b[0] - b[1], b[0], b[1]] # compute shading tangents if self.mesh.n is not None: ns = geo.normalize( o2w(b[0] * self.mesh.n[self.v] + b[1] * self.mesh.n[self.v + 1] + b[2] * self.mesh.n[self.v + 2])) else: ns = dg.nn if self.mesh.s is not None: ss = geo.normalize( o2w(b[0] * self.mesh.s[self.v] + b[1] * self.mesh.s[self.v + 1] + b[2] * self.mesh.s[self.v + 2])) else: ss = geo.normalize(dg.dpdu) ts = ss.cross(ns) if ts.sq_length() > 0.: ts.normalize() ss = ts.cross(ns) else: _, ss, ts = geo.coordinate_system(ns) # compute dndu and dndv if self.mesh.n is not None: du1 = uvs[0][0] - uvs[2][0] du2 = uvs[1][0] - uvs[2][0] dv1 = uvs[0][1] - uvs[2][1] dv2 = uvs[1][1] - uvs[2][1] dn1 = self.mesh.n[self.mesh.vertexIndex[self.v]] - self.mesh.n[ self.mesh.vertexIndex[self.v + 2]] dn2 = self.mesh.n[self.mesh.vertexIndex[self.v + 1]] - self.mesh.n[ self.mesh.vertexIndex[self.v + 2]] det = du1 * dv2 - du2 * dv1 if det == 0.: # choose an arbitrary system dndu = dndv = geo.Normal(0., 0., 0.) else: detInv = 1. / det dndu = (dv2 * dn1 - dv1 * dn2) * detInv dndv = (-du2 * dn1 + du1 * dn2) * detInv else: dndu = geo.Normal(0., 0., 0.) dndv = geo.Normal(0., 0., 0.) dgs = geo.DifferentialGeometry(dg.p, ss, ts, self.mesh.o2w(dndu), self.mesh.o2w(dndv), dg.u, dg.v, dg.shape) dgs.dudx = dg.dudx dgs.dvdx = dg.dvdx dgs.dudy = dg.dudy dgs.dvdy = dg.dvdy dgs.dpdx = dg.dpdx dgs.dpdy = dg.dpdy return dgs
def intersect( self, r: 'geo.Ray') -> (bool, FLOAT, FLOAT, 'geo.DifferentialGeometry'): """ Returns: - bool: specify whether intersects - tHit: hit point param - rEps: error tolerance - dg: geo.DifferentialGeometry object """ # transform ray to object space ray = self.w2o(r) # solve quad eqn A = ray.d.x * ray.d.x + ray.d.y * ray.d.y B = 2 * (ray.d.x * ray.o.x + ray.d.y * ray.o.y) C = ray.o.x * ray.o.x + ray.o.y * ray.o.y - self.radius * self.radius D = B * B - 4. * A * C if D <= 0.: return [False, None, None, None] # validate solutions [t0, t1] = np.roots([A, B, C]) if t0 > t1: t0, t1 = t1, t0 if t0 > ray.maxt or t1 < ray.mint: return [False, None, None, None] thit = t0 if t0 < ray.mint: thit = t1 if thit > ray.maxt: return [False, None, None, None] # cylinder hit position phit = ray(thit) phi = np.arctan2(phit.y, phit.x) if phi < 0.: phi += 2. * np.pi # test intersection against clipping params if phit.z < self.zmin or phit.z > self.zmax or phi > self.phiMax: if thit == t1 or t1 > ray.maxt: return [False, None, None, None] # try again with t1 thit = t1 phit = ray(thit) phi = np.arctan2(phit.y, phit.x) if phi < 0.: phi += 2. * np.pi if phit.z < self.zmin or phit.z > self.zmax or phi > self.phiMax: if thit == t1 or t1 > ray.maxt: return [False, None, None, None] # otherwise ray hits the cylinder # initialize the differential structure u = phi / self.phiMax v = (phit.z - self.zmin) / (self.thetaMax - self.thetaMin) # find derivatives dpdu = geo.Vector(-self.phiMax * phit.y, self.phiMax * phit.x, 0.) dpdv = geo.Vector(0., 0., self.zmax - self.zmin) # derivative of geo.Normals # given by Weingarten Eqn d2pduu = -self.phiMax * self.phiMax * geo.Vector(phit.x, phit.y, 0.) d2pduv = geo.Vector(0., 0., 0.) d2pdvv = geo.Vector(0., 0., 0.) # fundamental forms E = dpdu.dot(dpdu) F = dpdu.dot(dpdv) G = dpdv.dot(dpdv) N = geo.normalize(dpdu.cross(dpdv)) e = N.dot(d2pduu) f = N.dot(d2pduv) g = N.dot(d2pdvv) invEGFF = 1. / (E * G - F * F) dndu = geo.Normal.from_arr((f * F - e * G) * invEGFF * dpdu + (e * F - f * E) * invEGFF * dpdv) dndv = geo.Normal.from_arr((g * F - f * G) * invEGFF * dpdu + (f * F - g * E) * invEGFF * dpdv) o2w = self.o2w dg = geo.DifferentialGeometry(o2w(phit), o2w(dpdu), o2w(dpdv), o2w(dndu), o2w(dndv), u, v, self) return True, thit, EPS * thit, dg
def intersect(self, r: 'geo.Ray') -> [bool, FLOAT, FLOAT, 'geo.DifferentialGeometry']: """ Returns: - bool: specify whether intersects - tHit: hit point param - rEps: error tolerance - dg: geo.DifferentialGeometry object """ # transform ray to object space ray = self.w2o(r) # solve quad eqn A = ray.d.sq_length() B = 2 * ray.d.dot(ray.o) C = ray.o.sq_length() - self.radius * self.radius D = B * B - 4. * A * C if D <= 0.: return [False, None, None, None] # validate solutions [t0, t1] = np.roots([A, B, C]) if t0 > t1: t0, t1 = t1, t0 if t0 > ray.maxt or t1 < ray.mint: return [False, None, None, None] thit = t0 if t0 < ray.mint: thit = t1 if thit > ray.maxt: return [False, None, None, None] # sphere hit position phit = ray(thit) if phit.x == 0. and phit.y == 0.: phit.x = EPS * self.radius phi = np.arctan2(phit.y, phit.x) if phi < 0.: phi += 2. * np.pi # test intersection against clipping params if (self.zmin > -self.radius and phit.z < self.zmin) or \ (self.zmax < self.radius and phit.z > self.zmax) or \ phi > self.phiMax: if thit == t1 or t1 > ray.maxt: return [False, None, None, None] # try again with t1 thit = t1 phit = ray(thit) if phit.x == 0. and phit.y == 0.: phit.x = EPS * self.radius phi = np.arctan2(phit.y, phit.x) if phi < 0.: phi += 2. * np.pi if (self.zmin > -self.radius and phit.z < self.zmin) or \ (self.zmax < self.radius and phit.z > self.zmax) or \ phi > self.phiMax: return [False, None, None, None] # otherwise ray hits the sphere # initialize the differential structure u = phi / self.phiMax theta = np.arccos(np.clip(phit.z / self.radius, -1., 1.)) delta_theta = self.thetaMax - self.thetaMin v = (theta - self.thetaMin) * delta_theta # find derivatives dpdu = geo.Vector(-self.phiMax * phit.y, self.phiMax * phit.x, 0.) zrad = np.sqrt(phit.x * phit.x + phit.y * phit.y) inv_zrad = 1. / zrad cphi = phit.x * inv_zrad sphi = phit.y * inv_zrad dpdv = delta_theta \ * geo.Vector(phit.z * cphi, phit.z * sphi, -self.radius * np.sin(theta)) # derivative of Normals # given by Weingarten Eqn d2pduu = -self.phiMax * self.phiMax * geo.Vector(phit.x, phit.y, 0.) d2pduv = delta_theta * phit.z * self.phiMax * geo.Vector(-sphi, cphi, 0.) d2pdvv = -delta_theta * delta_theta * geo.Vector(phit.x, phit.y, phit.z) # fundamental forms E = dpdu.dot(dpdu) F = dpdu.dot(dpdv) G = dpdv.dot(dpdv) N = geo.normalize(dpdu.cross(dpdv)) e = N.dot(d2pduu) f = N.dot(d2pduv) g = N.dot(d2pdvv) invEGFF = 1. / (E * G - F * F) dndu = geo.Normal.from_arr((f * F - e * G) * invEGFF * dpdu + (e * F - f * E) * invEGFF * dpdv) dndv = geo.Normal.from_arr((g * F - f * G) * invEGFF * dpdu + (f * F - g * E) * invEGFF * dpdv) o2w = self.o2w dg = geo.DifferentialGeometry(o2w(phit), o2w(dpdu), o2w(dpdv), o2w(dndu), o2w(dndv), u, v, self) return True, thit, EPS * thit, dg
def test_normalize(self, vec): v = geo.normalize(vec) if vec.length() != 0.: vec /= vec.length() assert_elem_eq(vec, v)