Skip to content

Commit

Permalink
Merge pull request #19 from Enigmatisms/normal
Browse files Browse the repository at this point in the history
Shading normal & Normal/Bump Map & Acceleration
  • Loading branch information
Enigmatisms authored Jun 28, 2023
2 parents 1c1030b + 7906560 commit 01cc587
Show file tree
Hide file tree
Showing 27 changed files with 1,084 additions and 473 deletions.
71 changes: 71 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
{
"files.associations": {
"cctype": "cpp",
"clocale": "cpp",
"cmath": "cpp",
"cstdarg": "cpp",
"cstddef": "cpp",
"cstdio": "cpp",
"cstdlib": "cpp",
"cstring": "cpp",
"ctime": "cpp",
"cwchar": "cpp",
"cwctype": "cpp",
"array": "cpp",
"atomic": "cpp",
"strstream": "cpp",
"*.tcc": "cpp",
"bitset": "cpp",
"chrono": "cpp",
"cinttypes": "cpp",
"codecvt": "cpp",
"complex": "cpp",
"condition_variable": "cpp",
"cstdint": "cpp",
"deque": "cpp",
"forward_list": "cpp",
"list": "cpp",
"unordered_map": "cpp",
"unordered_set": "cpp",
"vector": "cpp",
"exception": "cpp",
"algorithm": "cpp",
"filesystem": "cpp",
"functional": "cpp",
"iterator": "cpp",
"map": "cpp",
"memory": "cpp",
"memory_resource": "cpp",
"numeric": "cpp",
"optional": "cpp",
"random": "cpp",
"ratio": "cpp",
"set": "cpp",
"string": "cpp",
"string_view": "cpp",
"system_error": "cpp",
"tuple": "cpp",
"type_traits": "cpp",
"utility": "cpp",
"fstream": "cpp",
"initializer_list": "cpp",
"iomanip": "cpp",
"iosfwd": "cpp",
"iostream": "cpp",
"istream": "cpp",
"limits": "cpp",
"mutex": "cpp",
"new": "cpp",
"ostream": "cpp",
"sstream": "cpp",
"stdexcept": "cpp",
"streambuf": "cpp",
"thread": "cpp",
"cfenv": "cpp",
"typeindex": "cpp",
"typeinfo": "cpp",
"valarray": "cpp",
"variant": "cpp",
"bit": "cpp"
}
}
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Bunny scenes are not uploaded in the repo (90k+ primitives).

##### Console output

Powered by rich console. Note that there is a speed bottle-neck for loading scene objects (see PR #16)
Powered by rich console. Note that the loading speed bottleneck (in the figure) no longer exist after [PR #19](https://github.com/Enigmatisms/AdaPT/pull/19) !

![Screenshot from 2023-05-31 12-36-59](https://github.com/Enigmatisms/AdaPT/assets/46109954/963448af-da1b-422d-bcf7-e44aa4808973)

Expand All @@ -53,7 +53,8 @@ Here are the features I currently implemented and support:
- A **unidirectional / bidirectional Monte-Carlo MIS path tracer**: supports as many bounce times as you wish, and the rendering process is based on Taichi Lang, therefore it can be very fast (not on the first run, the first run of a scene might take a long time due to taichi function inlining, especially for BDPT). The figures displayed above can be rendered within 15-20s (with cuda-backend, GPU supported). The rendering result is displayed incrementally, or maximum iteration number can be pre-set.
- **Volumetric path tracer** that supports uni/bidirectional path tracing in both bounded and unbounded condition
- A **transient renderer** with which you can visualize the propagation of the global radiance.
- Texture packing and texture mapping, see `scenes/bunny.xml` for an configuration example.
- Texture packing and texture mapping, see `scenes/bunny.xml` for an configuration example. We support bump map / normal map / roughness map (this is not tested) for now.
- Shading normal is supported for a smooth appearance.
- Rendering checkpointing and [rich](https://github.com/Textualize/rich) console pannel support.
- Ray tracing accelerating structure, for now, we only support `BVH`. `KD-tree` will be implemented in the future.
- Global / indirect illumination & Ability to handle simple caustics
Expand All @@ -69,7 +70,7 @@ BTW, I am just a starter in CG (ray-tracing stuffs) and Taichi Lang, so there WI

### Rendering Example

To run the rendering, use:
​To run the rendering, use:

```shell
# For bidirectional path tracer
Expand Down
123 changes: 62 additions & 61 deletions bxdf/brdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ def __init__(self, elem: xet.Element, no_setup = False):
self.kd_default = True
self.ks_default = True
self.kg_default = True
self.ka_default = True
self.uv_coords = None

texture_nodes = elem.findall("texture")
Expand Down Expand Up @@ -93,7 +92,7 @@ def export(self):
)

def __repr__(self) -> str:
return f"<{self.type.capitalize()} BRDF, default:[{int(self.kd_default), int(self.ks_default), int(self.kg_default), int(self.ka_default)}]>"
return f"<{self.type.capitalize()} BRDF, default:[{int(self.kd_default), int(self.ks_default), int(self.kg_default)}]>"

@ti.dataclass
class BRDF:
Expand All @@ -109,7 +108,7 @@ class BRDF:

# ======================= Blinn-Phong ========================
@ti.func
def eval_blinn_phong(self, ray_in: vec3, ray_out: vec3, normal: vec3, tex = INVALID):
def eval_blinn_phong(self, it:ti.template(), ray_in: vec3, ray_out: vec3):
"""
Normally, ray in is along the opposite direction of normal
Attention: ray_in (in backward tracing) is actually out-going direction (in forward tracing)
Expand All @@ -120,48 +119,47 @@ def eval_blinn_phong(self, ray_in: vec3, ray_out: vec3, normal: vec3, tex = INVA
half_way = half_way.normalized()
else:
half_way.fill(0.0)
dot_clamp = ti.max(0.0, tm.dot(half_way, normal))
dot_clamp = ti.max(0.0, tm.dot(half_way, it.n_s))
glossy = tm.pow(dot_clamp, self.k_g)
cosine_term = ti.max(0.0, tm.dot(normal, ray_out))
cosine_term = ti.max(0.0, tm.dot(it.n_s, ray_out))
# A modified Phong model (k_d + k_s should be smaller than 1, otherwise not physically plausible)
diffuse_color = ti.select(tex[0] < 0, self.k_d, tex)
diffuse_color = ti.select(it.is_tex_invalid(), self.k_d, it.tex)
return (diffuse_color + self.k_s * (0.5 * (self.k_g + 2.0) * glossy)) * INV_PI * cosine_term

@ti.func
def sample_blinn_phong(self, incid: vec3, normal: vec3, tex = INVALID):
def sample_blinn_phong(self, it:ti.template(), incid: vec3):
local_new_dir, pdf = cosine_hemisphere()
ray_out_d, _ = delocalize_rotate(normal, local_new_dir)
spec = self.eval_blinn_phong(incid, ray_out_d, normal, tex)
ray_out_d, _ = delocalize_rotate(it.n_s, local_new_dir)
spec = self.eval_blinn_phong(it, incid, ray_out_d)
return ray_out_d, spec, pdf

# ====================== Modified Phong =======================
@ti.func
def eval_mod_phong(self, ray_in: vec3, ray_out: vec3, normal: vec3):
dot_normal = tm.dot(normal, ray_out)
def eval_mod_phong(self, it: ti.template(), ray_in: vec3, ray_out: vec3):
dot_normal = tm.dot(it.n_s, ray_out)
spec = vec3([0, 0, 0])
if dot_normal > 0.0: # Phong model - specular part
reflect_d = (2 * normal * dot_normal - ray_out).normalized()
reflect_d = (2 * it.n_s * dot_normal - ray_out).normalized()
dot_view = ti.max(0.0, -tm.dot(ray_in, reflect_d)) # ray_in is on the opposite dir of reflected dir
glossy = tm.pow(dot_view, self.k_g) * self.k_s
spec = 0.5 * (self.k_g + 2.) * glossy * INV_PI * dot_normal
return spec

@ti.func
def sample_mod_phong(self, incid: vec3, normal: vec3, tex = INVALID):
def sample_mod_phong(self, it: ti.template(), incid: vec3):
# Sampling is more complicated
eps = ti.random(float)
ray_out_d = vec3([0, 1, 0])
spec = vec3([0, 0, 0])
diffuse_color = ti.select(tex[0] < 0, self.k_d, tex)
pdf = diffuse_color.max()
pdf = ti.select(it.is_tex_invalid(), self.k_d, it.tex).max() # the max element of diffuse color is the pdf of sampling diffusive component
if eps < pdf: # diffusive sampling
ray_out_d, spec, lmbt_pdf = self.sample_lambertian(normal, diffuse_color) # we use "texture" anyway, since it is converted
ray_out_d, spec, lmbt_pdf = self.sample_lambertian(it) # we use "texture" anyway, since it is converted
pdf *= lmbt_pdf
elif eps < pdf + self.k_s.max(): # specular sampling
local_new_dir, pdf = mod_phong_hemisphere(self.mean[2])
reflect_view = (-2 * normal * tm.dot(incid, normal) + incid).normalized()
reflect_view = (-2 * it.n_s * tm.dot(incid, it.n_s) + incid).normalized()
ray_out_d, _ = delocalize_rotate(reflect_view, local_new_dir)
spec = self.eval_mod_phong(incid, ray_out_d, normal)
spec = self.eval_mod_phong(it, incid, ray_out_d)
pdf *= self.k_s.max()
else: # zero contribution
# it doesn't matter even we don't return a valid ray_out_d
Expand All @@ -187,28 +185,28 @@ def fresnel_blend_dir(self, incid: vec3, half: vec3, normal: vec3, power_coeff:

@ti.func
def fresnel_cos2_sin2(self, half_vec: vec3, normal: vec3, R: mat3, dot_half: float):
transed_x = (R @ vec3([1, 0, 0])).normalized()
transed_x = (R @ vec3([1, 0, 0]))
cos_phi2 = tm.dot(transed_x, (half_vec - dot_half * normal).normalized()) ** 2 # azimuth angle of half vector
return cos_phi2, 1. - cos_phi2

@ti.func
def eval_fresnel_blend(self, ray_in: vec3, ray_out: vec3, normal: vec3, R: mat3, tex = INVALID):
def eval_fresnel_blend(self, it:ti.template(), ray_in: vec3, ray_out: vec3, R: mat3):
# specular part, note that ray out is actually incident light in forward tracing
half_vec = (ray_out - ray_in)
dot_out = tm.dot(normal, ray_out)
dot_out = tm.dot(it.n_s, ray_out)
spec = vec3([0, 0, 0])
if dot_out > 0. and ti.abs(half_vec).max() > 1e-4: # ray_in and ray_out not on the exact opposite direction
half_vec = half_vec.normalized()
dot_in = -tm.dot(normal, ray_in) # incident dot should always be positive (otherwise it won't hit this point)
dot_half = ti.abs(tm.dot(normal, half_vec))
dot_in = -tm.dot(it.n_s, ray_in) # incident dot should always be positive (otherwise it won't hit this point)
dot_half = ti.abs(tm.dot(it.n_s, half_vec))
dot_hk = ti.abs(tm.dot(half_vec, ray_out))
fresnel = schlick_fresnel(self.k_s, dot_hk)
cos_phi2, sin_phi2 = self.fresnel_cos2_sin2(half_vec, normal, R, dot_half)
cos_phi2, sin_phi2 = self.fresnel_cos2_sin2(half_vec, it.n_s, R, dot_half)
# k_g[2] should store sqrt((n_u + 1)(n_v + 1)) / 8pi
denom = dot_hk * tm.max(dot_in, dot_out)
specular = self.k_g[2] * tm.pow(dot_half, self.k_g[0] * cos_phi2 + self.k_g[1] * sin_phi2) * fresnel / denom
# diffusive part
diffuse_color = ti.select(tex[0] < 0, self.k_d, tex)
diffuse_color = ti.select(it.is_tex_invalid(), self.k_d, it.tex)
diffuse = 28. / (23. * tm.pi) * diffuse_color * (1. - self.k_s)
pow5_in = tm.pow(1. - dot_in / 2., 5)
pow5_out = tm.pow(1. - dot_out / 2., 5)
Expand All @@ -217,59 +215,59 @@ def eval_fresnel_blend(self, ray_in: vec3, ray_out: vec3, normal: vec3, R: mat3,
return spec

@ti.func
def sample_fresnel_blend(self, incid: vec3, normal: vec3, tex = INVALID):
def sample_fresnel_blend(self, it: ti.template(), incid: vec3):
local_new_dir, power_coeff = fresnel_hemisphere(self.k_g[0], self.k_g[1])
ray_half, R = delocalize_rotate(normal, local_new_dir)
ray_out_d, pdf, is_valid = self.fresnel_blend_dir(incid, ray_half, normal, power_coeff)
ray_half, R = delocalize_rotate(it.n_s, local_new_dir)
ray_out_d, pdf, is_valid = self.fresnel_blend_dir(incid, ray_half, it.n_s, power_coeff)
if ti.random(float) > 0.5:
ray_out_d, _s, _p = self.sample_lambertian(normal, tex)
pdf = 0.5 * (pdf + ti.abs(tm.dot(ray_out_d, normal)) * INV_PI)
spec = ti.select(is_valid, self.eval_fresnel_blend(incid, ray_out_d, normal, R), ZERO_V3)
ray_out_d, _s, _p = self.sample_lambertian(it)
pdf = 0.5 * (pdf + ti.abs(tm.dot(ray_out_d, it.n_s)) * INV_PI)
spec = ti.select(is_valid, self.eval_fresnel_blend(it, incid, ray_out_d, R), ZERO_V3)
return ray_out_d, spec, pdf

# ======================= Lambertian ========================
@ti.func
def eval_lambertian(self, ray_out: vec3, normal: vec3, tex = INVALID):
cosine_term = tm.max(0.0, tm.dot(normal, ray_out))
diffuse_color = ti.select(tex[0] < 0, self.k_d, tex)
def eval_lambertian(self, it: ti.template(), ray_out: vec3):
cosine_term = tm.max(0.0, tm.dot(it.n_s, ray_out))
diffuse_color = ti.select(it.is_tex_invalid(), self.k_d, it.tex)
return diffuse_color * INV_PI * cosine_term

@ti.func
def sample_lambertian(self, normal: vec3, tex = INVALID):
def sample_lambertian(self, it: ti.template()):
local_new_dir, pdf = cosine_hemisphere()
ray_out_d, _ = delocalize_rotate(normal, local_new_dir)
spec = self.eval_lambertian(ray_out_d, normal, tex)
ray_out_d, _ = delocalize_rotate(it.n_s, local_new_dir)
spec = self.eval_lambertian(it, ray_out_d)
return ray_out_d, spec, pdf

# ======================= Mirror-Specular ========================
@ti.func
def sample_specular(self, ray_in: vec3, normal: vec3, tex = INVALID):
ray_out_d, _ = inci_reflect_dir(ray_in, normal)
return ray_out_d, ti.select(tex[0] < 0, self.k_d, tex), 1.0
def sample_specular(self, it: ti.template(), ray_in: vec3):
ray_out_d, _ = inci_reflect_dir(ray_in, it.n_s)
return ray_out_d, ti.select(it.is_tex_invalid(), self.k_d, it.tex), 1.0

# ================================================================

@ti.func
def eval(self, incid: vec3, out: vec3, normal: vec3, tex = INVALID) -> vec3:
def eval(self, it: ti.template(), incid: vec3, out: vec3) -> vec3:
""" 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 tm.dot(incid, it.n_g) * tm.dot(out, it.n_g) < 0:
if self._type == 0: # Blinn-Phong
ret_spec = self.eval_blinn_phong(incid, out, normal, tex)
ret_spec = self.eval_blinn_phong(it, incid, out)
elif self._type == 1: # Lambertian
ret_spec = self.eval_lambertian(out, normal, tex)
ret_spec = self.eval_lambertian(it, out)
elif self._type == 4:
ret_spec = self.eval_mod_phong(incid, out, normal)
ret_spec = self.eval_mod_phong(it, incid, out)
elif self._type == 5:
R = rotation_between(vec3([0, 1, 0]), normal)
ret_spec = self.eval_fresnel_blend(incid, out, normal, R, tex)
R = rotation_between(vec3([0, 1, 0]), it.n_s)
ret_spec = self.eval_fresnel_blend(it, incid, out, R)
return ret_spec

@ti.func
def sample_new_rays(self, incid: vec3, normal: vec3, tex = INVALID):
def sample_new_rays(self, it:ti.template(), incid: vec3):
"""
All the sampling function will return: (1) new ray (direction) \\
(2) rendering equation transfer term (BRDF * cos term) (3) PDF
Expand All @@ -281,47 +279,50 @@ def sample_new_rays(self, incid: vec3, normal: vec3, tex = INVALID):
ret_spec = vec3([1, 1, 1])
pdf = 1.0
if self._type == 0: # Blinn-Phong
ret_dir, ret_spec, pdf = self.sample_blinn_phong(incid, normal, tex)
ret_dir, ret_spec, pdf = self.sample_blinn_phong(it, incid)
elif self._type == 1: # Lambertian
ret_dir, ret_spec, pdf = self.sample_lambertian(normal, tex)
ret_dir, ret_spec, pdf = self.sample_lambertian(it)
elif self._type == 2: # Specular - specular has no cosine attenuation
ret_dir, ret_spec, pdf = self.sample_specular(incid, normal, tex)
ret_dir, ret_spec, pdf = self.sample_specular(it, incid)
elif self._type == 4: # Modified-Phong
ret_dir, ret_spec, pdf = self.sample_mod_phong(incid, normal, tex)
ret_dir, ret_spec, pdf = self.sample_mod_phong(it, incid)
elif self._type == 5: # Fresnel-Blend
ret_dir, ret_spec, pdf = self.sample_fresnel_blend(incid, normal, tex)
ret_dir, ret_spec, pdf = self.sample_fresnel_blend(it, incid)
else:
print(f"Warnning: unknown or unsupported BRDF type: {self._type} during sampling.")
# Prevent shading normal from light leaking or accidental shadowing
ret_dot = tm.dot(ret_dir, it.n_g)
ret_spec = ti.select(ret_dot > 0, ret_spec, 0.)
return ret_dir, ret_spec, pdf

@ti.func
def get_pdf(self, outdir: vec3, normal: vec3, incid: vec3, tex = INVALID):
def get_pdf(self, it: ti.template(), outdir: vec3, incid: vec3):
"""
Solid angle PDF for a specific incident direction - BRDF sampling
Some PDF has nothing to do with backward incid (from eye to the surface), like diffusive
This PDF is actually the PDF of cosine-weighted term * BRDF function value
"""
pdf = 0.0
dot_outdir = tm.dot(normal, outdir)
dot_indir = tm.dot(normal, incid)
dot_outdir = tm.dot(it.n_s, outdir)
dot_indir = tm.dot(it.n_s, 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)
reflect_view, _ = inci_reflect_dir(incid, it.n_s)
dot_ref_out = tm.max(0., tm.dot(reflect_view, outdir))
diffuse_pdf = dot_outdir * INV_PI
specular_pdf = 0.5 * (glossiness + 1.) * INV_PI * tm.pow(dot_ref_out, glossiness)
diffuse_color = ti.select(tex[0] < 0, self.k_d, tex)
diffuse_color = ti.select(it.is_tex_invalid(), self.k_d, it.tex)
pdf = diffuse_color.max() * diffuse_pdf + self.k_s.max() * specular_pdf
elif self._type == 5:
half_vec = (outdir - incid).normalized()
dot_half = tm.dot(half_vec, normal)
R = rotation_between(vec3([0, 1, 0]), normal)
cos_phi2, sin_phi2 = self.fresnel_cos2_sin2(half_vec, normal, R, dot_half)
dot_half = tm.dot(half_vec, it.n_s)
R = rotation_between(vec3([0, 1, 0]), it.n_s)
cos_phi2, sin_phi2 = self.fresnel_cos2_sin2(half_vec, it.n_s, R, dot_half)
pdf = self.k_g[2] * tm.pow(dot_half, self.k_g[0] * cos_phi2 + self.k_g[1] * sin_phi2) / ti.abs(tm.dot(incid, half_vec))
pdf = 0.5 * (pdf + dot_outdir * INV_PI)
return pdf
Loading

0 comments on commit 01cc587

Please sign in to comment.