Skip to content

Commit

Permalink
Merge commit 'a9a1803760c8471965f3f4ad3a2ee73a8de9652a'
Browse files Browse the repository at this point in the history
  • Loading branch information
Enigmatisms committed Mar 5, 2023
2 parents c094a9a + a9a1803 commit 7be81cd
Show file tree
Hide file tree
Showing 30 changed files with 1,366 additions and 313 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
*.ini

cached/
fast_test/
ti_tests/**
**/__pycache__

!assets/*.png
61 changes: 33 additions & 28 deletions bxdf/brdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@
from la.cam_transform import *
from sampler.general_sampling import *
from scene.general_parser import rgb_parse
from renderer.constants import TRANSPORT_RAD, INV_PI

__all__ = ['BRDF_np', 'BRDF']

EPS = 1e-7
INV_PI = 1. / tm.pi

class BRDF_np:
"""
Expand Down Expand Up @@ -251,28 +251,33 @@ def sample_specular(self, ray_in: vec3, normal: vec3):

@ti.func
def eval(self, incid: vec3, out: vec3, normal: vec3) -> vec3:
""" Direct component reflectance """
ret_spec = vec3([1, 1, 1])
if self._type == 0: # Blinn-Phong
ret_spec = self.eval_blinn_phong(incid, out, normal)
elif self._type == 1: # Lambertian
ret_spec = self.eval_lambertian(out, normal)
elif self._type == 2: # Specular
ret_spec = self.eval_specular(incid, out, normal)
elif self._type == 4:
ret_spec = self.eval_mod_phong(incid, out, normal)
elif self._type == 5:
R = rotation_between(vec3([0, 1, 0]), normal)
ret_spec = self.eval_frensel_blend(incid, out, normal, R)
else:
print(f"Warnning: unknown or unsupported BRDF type: {self._type} during evaluation.")
""" Direct component reflectance
Every evaluation function does not output cosine weighted BSDF now
"""
ret_spec = vec3([0, 0, 0])
# For reflection, incident (in reverse direction) & outdir should be in the same hemisphere defined by the normal
if tm.dot(incid, normal) * tm.dot(out, normal) < 0:
if self._type == 0: # Blinn-Phong
ret_spec = self.eval_blinn_phong(incid, out, normal)
elif self._type == 1: # Lambertian
ret_spec = self.eval_lambertian(out, normal)
elif self._type == 2: # Specular
ret_spec = self.eval_specular(incid, out, normal)
elif self._type == 4:
ret_spec = self.eval_mod_phong(incid, out, normal)
elif self._type == 5:
R = rotation_between(vec3([0, 1, 0]), normal)
ret_spec = self.eval_frensel_blend(incid, out, normal, R)
else:
print(f"Warnning: unknown or unsupported BRDF type: {self._type} during evaluation.")
return ret_spec

@ti.func
def sample_new_rays(self, incid: vec3, normal: vec3):
"""
All the sampling function will return: (1) new ray (direction) \\
(2) rendering equation transfer term (BRDF * cos term) (3) PDF
mode for separating camera / light transport cosine term
"""
ret_dir = vec3([0, 1, 0])
ret_spec = vec3([1, 1, 1])
Expand All @@ -281,14 +286,14 @@ def sample_new_rays(self, incid: vec3, normal: vec3):
ret_dir, ret_spec, pdf = self.sample_blinn_phong(incid, normal)
elif self._type == 1: # Lambertian
ret_dir, ret_spec, pdf = self.sample_lambertian(normal)
elif self._type == 2: # Specular
elif self._type == 2: # Specular - specular has no cosine attenuation
ret_dir, ret_spec, pdf = self.sample_specular(incid, normal)
elif self._type == 4: # Modified-Phong
ret_dir, ret_spec, pdf = self.sample_mod_phong(incid, normal)
elif self._type == 5: # Frensel-Blend
ret_dir, ret_spec, pdf = self.sample_frensel_blend(incid, normal)
else:
print(f"Warnning: unknown or unsupported BRDF type: {self._type} during evaluation.")
print(f"Warnning: unknown or unsupported BRDF type: {self._type} during sampling.")
return ret_dir, ret_spec, pdf

@ti.func
Expand All @@ -300,20 +305,20 @@ def get_pdf(self, outdir: vec3, normal: vec3, incid: vec3):
"""
pdf = 0.0
dot_outdir = tm.dot(normal, outdir)
if self._type == 0:
pdf = tm.max(dot_outdir, 0.0) * INV_PI # dot is cosine term
elif self._type == 1:
pdf = tm.max(dot_outdir, 0.0) * INV_PI
elif self._type == 4:
if dot_outdir > 0.0:
dot_indir = tm.dot(normal, incid)
if dot_outdir * dot_indir < 0.: # same hemisphere
if self._type == 0:
pdf = dot_outdir * INV_PI # dot is cosine term
elif self._type == 1:
pdf = dot_outdir * INV_PI
elif self._type == 4:
glossiness = self.mean[2]
reflect_view, _ = inci_reflect_dir(incid, normal)
dot_ref_out = tm.max(0., tm.dot(reflect_view, outdir))
diffuse_pdf = tm.max(dot_outdir, 0.0) * INV_PI
diffuse_pdf = dot_outdir * INV_PI
specular_pdf = 0.5 * (glossiness + 1.) * INV_PI * tm.pow(dot_ref_out, glossiness)
pdf = self.k_d.max() * diffuse_pdf + self.k_s.max() * specular_pdf
elif self._type == 5:
if dot_outdir > 0.0:
elif self._type == 5:
half_vec = (outdir - incid).normalized()
dot_half = tm.dot(half_vec, normal)
R = rotation_between(vec3([0, 1, 0]), normal)
Expand Down
113 changes: 63 additions & 50 deletions bxdf/bsdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,10 @@
from sampler.general_sampling import *
from bxdf.brdf import BRDF_np
from bxdf.medium import Medium, Medium_np
from renderer.constants import TRANSPORT_RAD, INV_PI

__all__ = ['BSDF_np', 'BSDF']

INV_PI = 1. / tm.pi

class BSDF_np(BRDF_np):
"""
BSDF base-class,
Expand All @@ -46,8 +45,6 @@ def setup(self):
if self.type not in BSDF_np.__type_mapping:
raise NotImplementedError(f"Unknown BSDF type: {self.type}")
self.type_id = BSDF_np.__type_mapping[self.type]
if self.type_id < 1:
self.is_delta = True

def export(self):
return BSDF(
Expand All @@ -58,7 +55,7 @@ def export(self):
def __repr__(self) -> str:
return f"<{self.type.capitalize()} BSDF with {self.medium.__repr__()} >"


# TODO: Non-symmetry Due to Refraction
@ti.dataclass
class BSDF:
"""
Expand All @@ -67,7 +64,7 @@ class BSDF:
- transmission and reflection have independent distribution, yet transmission can be stochastic
"""
_type: int
is_delta: int # whether the BRDF is Dirac-delta-like
is_delta: int # whether the BRDF is Dirac-delta-like
k_d: vec3 # diffusive coefficient (albedo)
k_s: vec3 # specular coefficient
k_g: vec3 # glossiness coefficient
Expand All @@ -76,7 +73,7 @@ class BSDF:

# ========================= Deterministic Refraction =========================
@ti.func
def sample_det_refraction(self, incid: vec3, normal: vec3, medium):
def sample_det_refraction(self, incid: vec3, normal: vec3, medium, mode):
"""
Deterministic refraction sampling - Surface reflection is pure mirror specular \\
other (Medium) could be incident medium or refraction medium, depending on \\
Expand All @@ -88,27 +85,33 @@ def sample_det_refraction(self, incid: vec3, normal: vec3, medium):
nr = ti.select(entering_this, self.medium.ior, medium.ior)
ret_pdf = 1.0
ret_dir = vec3([0, 1, 0])
ret_int = self.k_d
if is_total_reflection(dot_normal, ni, nr):
# Only sample BSDF reflection
ret_dir = (incid - 2 * normal * dot_normal).normalized()
else:
refra_vec, _ = snell_refraction(incid, normal, dot_normal, ni, nr)
reflect_ratio = frensel_equation(ni, nr, ti.abs(dot_normal), ti.abs(tm.dot(refra_vec, normal)))
if ti.random(float) > reflect_ratio: # refraction
ret_pdf = 1. - reflect_ratio
ret_dir = refra_vec
else: # reflection
refra_vec, valid_ref = snell_refraction(incid, normal, dot_normal, ni, nr)
if valid_ref:
reflect_ratio = frensel_equation(ni, nr, ti.abs(dot_normal), ti.abs(tm.dot(refra_vec, normal)))
if ti.random(float) > reflect_ratio: # refraction
ret_pdf = 1. - reflect_ratio
ret_dir = refra_vec
if mode == TRANSPORT_RAD: # non-symmetry effect
ret_int *= (ni * ni) / (nr * nr)
else: # reflection
ret_dir = (incid - 2 * normal * dot_normal).normalized()
ret_pdf = reflect_ratio
else:
ret_dir = (incid - 2 * normal * dot_normal).normalized()
ret_pdf = reflect_ratio
return ret_dir, self.k_d * ret_pdf, ret_pdf
return ret_dir, ret_int * ret_pdf, ret_pdf

@ti.func
def eval_det_refraction(self, ray_in: vec3, ray_out: vec3, normal: vec3, medium):
def eval_det_refraction(self, ray_in: vec3, ray_out: vec3, normal: vec3, medium, mode):
dot_out = tm.dot(ray_out, normal)
entering_this = dot_out < 0
# notice that eval uses ray_out while sampling uses ray_in, therefore nr & ni have different order
nr = ti.select(entering_this, medium.ior, self.medium.ior)
ni = ti.select(entering_this, self.medium.ior, medium.ior)
ni = ti.select(entering_this, medium.ior, self.medium.ior)
nr = ti.select(entering_this, self.medium.ior, medium.ior)
ret_int = vec3([0, 0, 0])
if is_total_reflection(dot_out, ni, nr):
ref_dir = (ray_out - 2 * normal * dot_out).normalized()
Expand All @@ -117,55 +120,65 @@ def eval_det_refraction(self, ray_in: vec3, ray_out: vec3, normal: vec3, medium)
else:
# in sampling: ray_in points to the intersection, here ray_out points away from the intersection
ref_dir = (ray_out - 2 * normal * dot_out).normalized()
refra_vec, _ = snell_refraction(ray_out, normal, dot_out, ni, nr)
if tm.dot(refra_vec, ray_in) > 1 - 1e-4: # ray_in close to refracted dir
refra_vec, valid_ref = snell_refraction(ray_out, normal, dot_out, ni, nr)
if valid_ref:
reflect_ratio = frensel_equation(ni, nr, ti.abs(dot_out), ti.abs(tm.dot(refra_vec, normal)))
ret_int = self.k_d * (1. - reflect_ratio)
elif tm.dot(ref_dir, ray_in) > 1 - 1e-4: # ray_in close to reflected dir
reflect_ratio = frensel_equation(ni, nr, ti.abs(dot_out), ti.abs(tm.dot(refra_vec, normal)))
ret_int = self.k_d * reflect_ratio
return ret_int

# ========================= Null surface =========================
@ti.func
def sample_null(self, incid):
return incid, vec3([1, 1, 1]), 1.0

@ti.func
def eval_null(self, ray_in: vec3, ray_out: vec3):
ret_int = vec3([0, 0, 0])
if tm.dot(ray_in, ray_out) > 1 - 1e-5:
ret_int = vec3([1, 1, 1])
if tm.dot(refra_vec, ray_in) > 1 - 1e-4: # ray_in close to refracted dir
ret_int = self.k_d * (1. - reflect_ratio)
if mode == TRANSPORT_RAD: # consider non-symmetric effect due to different transport mode
ret_int *= (ni * ni) / (nr * nr)
elif tm.dot(ref_dir, ray_in) > 1 - 1e-4: # ray_in close to reflected dir
ret_int = self.k_d * reflect_ratio
else:
if tm.dot(ref_dir, ray_in) > 1 - 1e-4: # ray_in close to reflected dir
ret_int = self.k_d
return ret_int
# ========================= General operations =========================

@ti.func
def get_pdf(self, outdir: vec3, normal: vec3, incid: vec3, medium):
""" TODO: currently, deterministic refraction yields PDF = 0.0"""
return 0.0
""" TODO: currently, deterministic refraction / Null transmission yields PDF = 0.0 """
pdf = 0.
if self._type == -1:
pdf = ti.select(tm.dot(incid, outdir) > 1 - 1e-4, 1., 0.)
elif self._type == 0:
dot_out = tm.dot(outdir, normal)
entering_this = dot_out < 0
# notice that eval uses ray_out while sampling uses ray_in, therefore nr & ni have different order
ni = ti.select(entering_this, medium.ior, self.medium.ior)
nr = ti.select(entering_this, self.medium.ior, medium.ior)
ref_dir = (outdir - 2 * normal * dot_out).normalized()
refra_vec, valid_refra = snell_refraction(outdir, normal, dot_out, ni, nr)
if valid_refra: # not total reflection, so there is not only one possible choice
reflect_ratio = frensel_equation(ni, nr, ti.abs(dot_out), ti.abs(tm.dot(refra_vec, normal)))
if tm.dot(refra_vec, incid) > 1 - 1e-4: # ray_in close to refracted dir
pdf = 1. - reflect_ratio
elif tm.dot(ref_dir, incid) > 1 - 1e-4: # ray_in close to reflected dir
pdf = reflect_ratio
else:
if tm.dot(ref_dir, incid) > 1 - 1e-4:
pdf = 1.
return pdf

@ti.func
def is_non_null(self): # null surface is -1
return self._type >= 0

# ========================= Surface interactions ============================
@ti.func
def eval_surf(self, incid: vec3, out: vec3, normal: vec3, medium) -> vec3:
ret_spec = vec3([1, 1, 1])
def eval_surf(self, incid: vec3, out: vec3, normal: vec3, medium, mode) -> vec3:
ret_spec = vec3([0, 0, 0])
if self._type == 0:
ret_spec = self.eval_det_refraction(incid, out, normal, medium)
if self._type == -1:
ret_spec = self.eval_null(incid, out)
ret_spec = self.eval_det_refraction(incid, out, normal, medium, mode)
return ret_spec

@ti.func
def sample_surf_rays(self, incid: vec3, normal: vec3, medium):
ret_dir = vec3([0, 1, 0])
ret_spec = vec3([1, 1, 1])
pdf = 1.0
def sample_surf_rays(self, incid: vec3, normal: vec3, medium, mode):
# TODO: we need mode here (and in eval)
ret_dir = vec3([0, 0, 0])
ret_spec = vec3([0, 0, 0])
pdf = 0.0
if self._type == 0:
ret_dir, ret_spec, pdf = self.sample_det_refraction(incid, normal, medium)
elif self._type == -1:
ret_dir, ret_spec, pdf = self.sample_null(incid)
ret_dir, ret_spec, pdf = self.sample_det_refraction(incid, normal, medium, mode)
return ret_dir, ret_spec, pdf

24 changes: 11 additions & 13 deletions bxdf/medium.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ def export(self):
)

def __repr__(self):
return f"<Medium {self.type_name.capitalize()} with ior {self.ior:.3f}, extinction: {self.u_e}>"
return f"<Medium {self.type_name.capitalize()} with ior {self.ior:.3f}, extinction: {self.u_e}, scattering: {self.u_s}>"

@ti.dataclass
class Medium:
Expand All @@ -75,15 +75,9 @@ def is_scattering(self): # check whether the current medium is scattering medi

@ti.func
def transmittance(self, depth: float):
transmittance = ti.exp(-self.u_e * depth)
# transmitted without being scattered (PDF)
transmittance /= self.pdf_no_scatter(depth)
return transmittance
return ti.exp(-self.u_e * depth)

@ti.func
def pdf_no_scatter(self, depth):
return ti.max(ti.exp(-self.u_e * depth).sum() / 3., 1e-5) # self.u_e * depth < 11.5 (log 1e-5 \approx -11.5)

@ti.func
def sample_mfp(self, max_depth):
random_ue = random_rgb(self.u_e)
Expand All @@ -92,20 +86,24 @@ def sample_mfp(self, max_depth):
is_medium_interact = False
if sample_t >= max_depth:
sample_t = max_depth
pdf = self.pdf_no_scatter(max_depth)
beta = ti.exp(-self.u_e * max_depth) / pdf
tr = ti.exp(-self.u_e * max_depth)
pdf = tr.sum() / 3.
pdf = ti.select(pdf > 0., pdf, 1.)
beta = tr / pdf
else:
is_medium_interact = True
pdf = (self.u_e * ti.exp(-self.u_e * sample_t)).sum() / 3.
beta = ti.exp(-self.u_e * sample_t) * self.u_s / pdf
tr = ti.exp(-self.u_e * sample_t)
pdf = (self.u_e * tr).sum() / 3.
pdf = ti.select(pdf > 0., pdf, 1.)
beta = tr * self.u_s / pdf
return is_medium_interact, sample_t, beta

# ================== medium sampling & eval =======================

@ti.func
def sample_new_rays(self, incid: vec3):
ret_dir = vec3([0, 1, 0])
ret_spec = vec3([1, 1, 1])
ret_dir = incid
ret_pdf = 1.0
if self.is_scattering(): # medium interaction - evaluate phase function (currently output a float)
local_new_dir, ret_pdf = self.ph.sample_p(incid) # local frame ray_dir should be transformed
Expand Down
9 changes: 4 additions & 5 deletions bxdf/phase.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,18 @@
import taichi.math as tm
from taichi.math import vec3
from sampler.phase_sampling import *

__inv_2pi__ = 0.5 / tm.pi
from renderer.constants import INV_2PI

@ti.func
def phase_hg(cos_theta: float, g: float):
g2 = g * g
denom = 1. + g2 + 2. * g * cos_theta
return (1. - g2) / (ti.sqrt(denom) * denom) * 0.5 * __inv_2pi__
denom = 1. + g2 - 2. * g * cos_theta
return (1. - g2) / (ti.sqrt(denom) * denom) * 0.5 * INV_2PI

# ============== Rayleigh ================
@ti.func
def phase_rayleigh(cos_theta: float):
return 0.375 * __inv_2pi__ * (1. + cos_theta * cos_theta)
return 0.375 * INV_2PI * (1. + cos_theta * cos_theta)

@ti.dataclass
class PhaseFunction:
Expand Down
Loading

0 comments on commit 7be81cd

Please sign in to comment.