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(Pcam, geo.Vector(0., 0., 1.), 0., np.inf) # 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 __init__(self, c2w: 'trans.AnimatedTransform', scr_win: [FLOAT], s_open: FLOAT, s_close: FLOAT, lensr: FLOAT, focald: FLOAT, fov: FLOAT, f: 'Film'): super().__init__(c2w, trans.Transform.perspective(fov, .001, 1000.), # non-raster based, set arbitrarily scr_win, s_open, s_close, lensr, focald, f) # compute differential changes in origin self.dxCam = self.r2c(geo.Point(1., 0., 0.)) - self.r2c(geo.Point(0., 0., 0.)) self.dyCam = self.r2c(geo.Point(0., 1., 0.)) - self.r2c(geo.Point(0., 0., 0.))
def test_orthographic(self): znear = rng() zfar = znear + rng() * VAR t = Transform.orthographic(znear, zfar) p = geo.Point(rng(), rng(), znear) assert_almost_eq(t(p).z, 0.) p = geo.Point(rng(), rng(), zfar) assert_almost_eq(t(p).z, 1.)
def test_bbox_extent(self): p = geo.Point( 1., 1., 1., ) for i in range(3): p[i] = 2. b = geo.BBox(geo.Point(0., 0., 0.), p) assert b.maximum_extent() == i
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 test_rotate_bbox(self): t = Transform.rotate(180., geo.Vector(1., 1., 1.)) p1 = geo.Point(0., 0., 0.) p2 = geo.Point(1., 1., 1.) box = geo.BBox(p1, p2) b = t(box) pmax = t(box.pMax) pmin = t(box.pMin) assert_elem_eq(b.pMin, pmin) assert_elem_eq(b.pMax, pmax) bb = t(b) pmax = t(pmax) pmin = t(pmin) assert_elem_eq(bb.pMin, pmin) assert_elem_eq(bb.pMax, pmax)
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 __call__(self, dg: 'geo.DifferentialGeometry') -> [FLOAT]: v = dg.p - geo.Point(0., 0., 0.) return [ self.ds + v.dot(self.vs), self.dt + v.dot(self.vt), dg.dpdx.dot(self.vs), dg.dpdx.dot(self.vt), dg.dpdy.dot(self.vs), dg.dpdy.dot(self.vt) ]
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 __init__(self, l2w: 'trans.Transform', intensity: 'Spectrum', width: FLOAT, falloff: FLOAT): """ width: Overall angular width of the cone fall: angle at which falloff starts """ super().__init__(l2w) self.pos = l2w(geo.Point(0., 0., 0.)) # where the light is positioned in the world self.intensity = intensity self.cos_width = np.cos(np.deg2rad(width)) self.cos_falloff = np.cos(np.deg2rad(falloff))
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 test_perspective(self): znear = rng() * VAR zfar = znear + rng() * VAR fov = rng() * 45. + 45. t = Transform.perspective(fov, znear, zfar) p = geo.Point(znear * rng(), znear * rng(), znear + rng() * (zfar - znear)) pp = t(p) assert_almost_eq(pp.z, (p.z - znear) * zfar / ((zfar - znear) * p.z)) assert_almost_eq(pp.x, p.x / (p.z * np.tan(np.deg2rad(.5 * fov)))) assert_almost_eq(pp.y, p.y / (p.z * np.tan(np.deg2rad(.5 * fov))))
def pdf_p(self, pnt: 'geo.Point', wi: 'geo.Vector') -> FLOAT: ctr = self.o2w(geo.Point(0., 0., 0.)) # return uniform weight if inside if pnt.sq_dist(ctr) - self.radius * self.radius < EPS: return super().pdf_p(pnt, wi) # general weight 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_cone_pdf return uniform_cone_pdf(ct_max)
def test_bbox_intersect(self): b = geo.BBox(geo.Point(1., 1., 1.), geo.Point(2., 2., 2.)) ray = geo.Ray(geo.Point(0., 0., 0.), geo.Vector(1., 1., 1.)) hit, t1, t2 = b.intersect_p(ray) assert hit assert_almost_eq(t1, 1.) assert_almost_eq(t2, 2.) # ambiguity in this case ray = geo.Ray(geo.Point(1. - EPS, 1. - EPS, 0.), geo.Vector(-EPS, -EPS, 1.)) hit, t1, t2 = b.intersect_p(ray) assert not hit assert_almost_eq(t1, 0.) assert_almost_eq(t2, 0.) ray = geo.Ray(geo.Point(1. - EPS, 1. - EPS, 1. - EPS), geo.Vector(-1., -1., -1.)) hit, t1, t2 = b.intersect_p(ray) assert not hit assert_almost_eq(t1, 0.) assert_almost_eq(t2, 0.) ray = geo.Ray(geo.Point(0., 0., 0.), geo.Vector(-1., -1., -1.)) hit, t1, t2 = b.intersect_p(ray) assert not hit assert_almost_eq(t1, 0.) assert_almost_eq(t2, 0.)
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 __call__(self, arg, dtype=None): if isinstance(arg, geo.Point) or (isinstance(arg, np.ndarray) and dtype == geo.Point): res = self.m[0:4, 0:3].dot(arg) + self.m[0:4, 3] p = geo.Point(res[0], res[1], res[2]) if util.ne_unity(res[3]): p /= res[3] return p elif isinstance(arg, geo.Vector) or (isinstance(arg, np.ndarray) and dtype == geo.Vector): res = self.m[0:3, 0:3].dot(arg) return geo.Vector(res[0], res[1], res[2]) elif isinstance(arg, geo.Normal) or (isinstance(arg, np.ndarray) and dtype == geo.Normal): # must be transformed by inverse transpose res = self.m_inv[0:3, 0:3].T.dot(arg) return geo.Normal(res[0], res[1], res[2]) elif isinstance(arg, geo.RayDifferential): r = geo.RayDifferential.from_rd(arg) r.o = self(r.o) r.d = self(r.d) return r elif isinstance(arg, geo.Ray): r = geo.Ray.from_ray(arg) r.o = self(r.o) r.d = self(r.d) return r elif isinstance(arg, geo.BBox): res = geo.BBox.from_bbox(arg) x = geo.Vector(res.pMax.x - res.pMin.x, 0., 0.) y = geo.Vector(0., res.pMax.y - res.pMin.y, 0.) z = geo.Vector(0., 0., res.pMax.z - res.pMin.z) res.pMin = self(res.pMin) x = self(x) y = self(y) z = self(z) res.pMax = res.pMin + (x + y + z) return res else: raise TypeError( 'Transform can only be called on geo.Point, geo.Vector, geo.Normal, geo.Ray or geo.geo.BBox' )
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 brdf_remap(wo: 'geo.Vector', wi: 'geo.Vector') -> 'geo.Point': """ Mapping regularly sampled BRDF using Marschner, 1998 """ dphi = geo.spherical_phi(wi) - geo.spherical_phi(wo) if dphi < 0.: dphi += 2. * PI if dphi > 2. * PI: dphi -= 2. * PI if dphi > PI: dphi = 2. * PI - dphi return geo.Point( sin_theta(wi) * sin_theta(wo), dphi * INV_PI, cos_theta(wi) * cos_theta(wo))
def generate_ray(self, sample: 'CameraSample') -> [FLOAT, 'geo.Ray']: """ Generate ray based on image sample. Returned ray direction is normalized """ time = util.lerp(sample.time, self.s_open, self.s_close) # compute ray direction theta = np.pi * sample.imageY / self.film.yResolution phi = 2 * np.pi * sample.imageX / self.film.xResolution stheta = np.sin(theta) ray = self.c2w( geo.Ray( geo.Point(0., 0., 0.), geo.Vector(stheta * np.cos(phi), np.cos(theta), stheta * np.sin(phi)), 0., np.inf, time)) return [1., ray]
def __init__(self, l2w: 'trans.Transform', intensity: 'Spectrum', texname: str, fov: FLOAT): super().__init__(l2w) self.pos = l2w(geo.Point(0., 0., 0.)) # where the light is positioned in the world self.intensity = intensity # create a MIPMap try: import pytracer.utility.imageio as iio texels = iio.read_image(texname) width, height = np.shape(texels) except: print('src.core.texture.{}.get_texture(): cannot process file {}, ' 'use default one-valued MIPMap'.format(self.__class__, texname)) texels = None width, height = 0, 0 if texels is not None: from pytracer.texture import MIPMap from pytracer.spectral import RGBSpectrum self.projMap = MIPMap(RGBSpectrum, texels) else: self.projMap = None # init projection matrix aspect = width / height if aspect > 1.: self.scr_x0 = -aspect self.scr_x1 = aspect self.scr_y0 = -1. self.scr_y1 = 1. else: self.scr_x0 = -1. self.scr_x1 = 1. self.scr_y0 = -1. / aspect self.scr_y1 = 1. / aspect self.hither = EPS self.yon = 1e30 self.proj_trans = trans.Transform.perspective(fov, self.hither, self.yon) # compute cosine of cone self.cos_width = np.cos(np.arctan(np.tan(np.deg2rad(fov) / 2.) * np.sqrt(1. + 1. / (aspect * aspect))))
def __init__(self, l2w: 'trans.Transform', intensity: 'Spectrum', texname: str): super().__init__(l2w) self.pos = l2w(geo.Point(0., 0., 0.)) # where the light is positioned in the world self.intensity = intensity # create a MIPMap try: import pytracer.utility.imageio as iio texels = iio.read_image(texname) width, height = np.shape(texels) except: print('src.core.texture.{}.get_texture(): cannot process file {}, ' 'use default one-valued MIPMap'.format(self.__class__, texname)) texels = None if texels is not None: from pytracer.texture import MIPMap from pytracer.spectral import RGBSpectrum self.MIPMap = MIPMap(RGBSpectrum, texels) else: self.MIPMap = None
def __init__(self, l2w: 'trans.Transform', l: 'Spectrum', ns: INT, texmap: str=None): """ width: Overall angular width of the cone fall: angle at which falloff starts """ super().__init__(l2w, ns) self.pos = l2w(geo.Point(0., 0., 0.)) # where the light is positioned in the world texels = np.array([[l.to_rgb()]]) width, height = 1, 1 if texmap is not None: try: import pytracer.utility.imageio as iio texels = iio.read_image(texmap) width, height = np.shape(texels) except: print('src.core.texture.{}.get_texture(): cannot process file {}, ' 'use default one-valued MIPMap'.format(self.__class__, texmap)) texels = np.array([[l.to_rgb()]]) width, height = 1, 1 from pytracer.texture import MIPMap from pytracer.spectral import RGBSpectrum self.radMap = MIPMap(RGBSpectrum, texels) # init sampling PDFs <725> # compute image for envir. map img = np.empty([height, width], dtype=FLOAT) filt = 1. / max(width, height) for v in range(height): vp = v / height st = np.sin(PI * (v + .5) / height) for u in range(width): up = u / width img[v][u] = self.radMap.look_up([up, vp, filt]).y() # compute sampling distribution self.dist = mc.Distribution2D(img)
def __projection(self, w: 'geo.Vector') -> 'Spectrum': """ __projection() Utility method to determine the amount of light projected in the given direction. """ wl = self.w2l(w) # discard directions behind proj light if wl.z < self.hither: return Spectrum(0.) # project point onto plane pl = self.proj_trans(geo.Point(wl.x, wl.y, wl.z)) if pl.x < self.scr_x0 or pl.x > self.scr_x1 or \ pl.y < self.scr_y0 or pl.y > self.scr_y1: return Spectrum(0.) if self.projMap is None: return Spectrum(1.) s = (pl.x - self.scr_x0) / (self.scr_x1 - self.scr_x0) t = (pl.y - self.scr_y0) / (self.scr_y1 - self.scr_y0) return Spectrum(self.projMap.look_up([s, t]), SpectrumType.ILLUMINANT)
def object_bound(self) -> 'geo.BBox': return geo.BBox(geo.Point(-self.radius, -self.radius, self.zmin), geo.Point(self.radius, self.radius, self.zmax))
def object_bound(self) -> 'geo.BBox': return geo.BBox(geo.Point(-self.radius, -self.radius, self.height), geo.Point(self.radius, self.radius, self.height))
def test_bbox_bounding_sphere(self): b = geo.BBox(geo.Point(0., 0., 0.), geo.Point(1., 1., 1.)) ctr, rad = b.bounding_sphere() assert ctr == geo.Point(.5, .5, .5) assert_almost_eq(rad, np.sqrt(3) / 2)
def test_bbox_lerp(self): b = geo.BBox(geo.Point(0., 0., 0.), geo.Point(1., 1., 1.)) tx, ty, tz = rng(3) p = b.lerp(tx, ty, tz) assert_almost_eq(p.sq_length(), tx * tx + ty * ty + tz * tz)
def test_bbox_offset(self): b = geo.BBox(geo.Point(0., 0., 0.), geo.Point(1., 1., 1.)) tx, ty, tz = rng(3) p = b.lerp(tx, ty, tz) v = b.offset(p) assert_elem_eq(v, [tx, ty, tz])
from pytracer.transform import Transform import pytracer.transform.quat as quat N_TEST_CASE = 5 VAR = 10. EPS = 6 np.random.seed(1) rng = np.random.rand test_data = { 'vector': [geo.Vector(rng(), rng(), rng()) * VAR for _ in range(N_TEST_CASE)], 'normal': [geo.Normal(rng(), rng(), rng()) * VAR for _ in range(N_TEST_CASE)], 'point': [geo.Point(rng(), rng(), rng()) * VAR for _ in range(N_TEST_CASE)], 'quat': [ quat.Quaternion(rng(), rng(), rng(), rng()) * VAR for _ in range(N_TEST_CASE) ], } test_data['vector'].extend([ geo.Vector(0., 0., 0.), geo.Vector(1., 0., 0.), geo.Vector( 0., 1., 0., ), geo.Vector(1., 0., 0.) ])
def __init__(self, l2w: 'trans.Transform', intensity: 'Spectrum'): super().__init__(l2w) self.pos = l2w(geo.Point(0., 0., 0.)) # where the light is positioned in the world self.intensity = intensity