use std::f32::consts::PI;
use glam::{Vec2, Vec3, Vec4};
use super::prepare::{Draw, LightKind, Lights};
use super::reflection::ReflectionMap;
use super::shadow::ShadowMap;
use crate::material::Shading;
use crate::math::smoothstep;
use crate::scene::{Background, Fog};
use crate::{Blending, Environment};
pub(crate) struct ShadeContext<'a> {
pub lights: &'a Lights,
pub shadows: &'a [Option<ShadowMap>],
pub environment: &'a Environment,
pub baked: Option<&'a crate::EnvironmentMap>,
pub fog: Option<Fog>,
pub background: &'a Background,
pub camera_position: Vec3,
pub ortho_forward: Option<Vec3>,
pub reflection: Option<&'a ReflectionMap>,
pub viewport: Vec2,
}
impl ShadeContext<'_> {
fn shadow(&self, index: Option<usize>) -> Option<&ShadowMap> {
self.shadows.get(index?)?.as_ref()
}
pub(crate) fn env_radiance(&self, dir: Vec3, roughness: f32) -> Vec3 {
match (self.environment, self.baked) {
(Environment::Studio(studio), Some(map)) => {
map.sample(
crate::environment::rotate_y(dir, -studio.rotation),
roughness,
) * studio.intensity
}
(environment, _) => environment.radiance(dir, roughness),
}
}
}
pub(crate) struct Surface {
pub position: Vec3,
pub normal: Vec3,
pub face_normal: Vec3,
pub uv: Vec2,
pub color: Vec4,
pub uv_dx: Vec2,
pub uv_dy: Vec2,
pub dpdu: Vec3,
pub dpdv: Vec3,
pub normal_variation: f32,
pub screen: Vec2,
}
pub(crate) fn shade(ctx: &ShadeContext, draw: &Draw, s: &Surface) -> Vec4 {
let m = draw.material;
let to_camera = ctx.camera_position - s.position;
let distance = to_camera.length();
let v = match ctx.ortho_forward {
Some(forward) => -forward,
None => to_camera / distance.max(1e-6),
};
let face = if s.face_normal.dot(v) < 0.0 {
-s.face_normal
} else {
s.face_normal
};
let mut n = if m.flat_shading { face } else { s.normal };
let sample = |map: &crate::Texture| map.sample_grad(s.uv, s.uv_dx, s.uv_dy);
if draw.has_uv
&& let Some(map) = &m.normal_map
{
n = bend(n, s.dpdu, s.dpdv, sample(map), m.normal_scale);
}
if n.dot(face) < 0.0 {
n = -n;
}
let n_dot_v = n.dot(v).max(1e-4);
let mut base = m.color.0;
let mut alpha = m.opacity;
if draw.has_color {
base *= s.color.truncate();
alpha *= s.color.w;
}
if draw.has_uv
&& let Some(map) = &m.map
{
let texel = sample(map);
base *= texel.truncate();
alpha *= texel.w;
}
let mut emissive = m.emissive.0 * m.emissive_intensity;
if draw.has_uv
&& let Some(map) = &m.emissive_map
{
emissive *= sample(map).truncate();
}
let mut rgb = match m.shading {
Shading::Unlit => base + emissive,
Shading::Normals => n * 0.5 + 0.5,
Shading::ShadowCatcher => {
let mut lit = 1.0;
for light in &ctx.lights.direct {
if let LightKind::Directional { direction, shadow } = &light.kind
&& let Some(map) = ctx.shadow(*shadow)
{
lit *= map.visibility(s.position, face, face.dot(-*direction));
}
}
return Vec4::new(0.0, 0.0, 0.0, (1.0 - lit) * m.opacity);
}
Shading::Standard => standard(ctx, draw, s, base, n, face, v, n_dot_v) + emissive,
};
if m.fog
&& let Some(fog) = ctx.fog
{
let f = fog.factor(distance);
rgb = match m.blending {
Blending::Additive => rgb * (1.0 - f),
Blending::Normal => rgb.lerp(fog.color().0, f),
};
}
let rgb = rgb.clamp(Vec3::ZERO, Vec3::splat(30000.0));
let alpha = if m.is_blended() { alpha } else { 1.0 };
match m.blending {
Blending::Additive => (rgb * alpha).extend(0.0),
Blending::Normal => (rgb * alpha).extend(alpha),
}
}
fn bend(n: Vec3, dpdu: Vec3, dpdv: Vec3, texel: Vec4, scale: f32) -> Vec3 {
let m = texel.truncate() * 2.0 - 1.0;
let t = (dpdu - n * n.dot(dpdu)).normalize_or_zero();
let b = (dpdv - n * n.dot(dpdv)).normalize_or_zero();
(t * (m.x * scale) - b * (m.y * scale) + n * m.z).normalize_or(n)
}
pub(crate) fn uv_directions(e1: Vec3, e2: Vec3, d1: Vec2, d2: Vec2) -> (Vec3, Vec3) {
let det = d1.perp_dot(d2);
let sign = if det == 0.0 { 0.0 } else { det.signum() };
(
(e1 * d2.y - e2 * d1.y) * sign,
(e2 * d1.x - e1 * d2.x) * sign,
)
}
#[allow(clippy::too_many_arguments)]
fn standard(
ctx: &ShadeContext,
draw: &Draw,
s: &Surface,
base: Vec3,
n: Vec3,
face: Vec3,
v: Vec3,
n_dot_v: f32,
) -> Vec3 {
let m = draw.material;
let mut metalness = m.metalness;
let mut roughness = m.roughness;
if draw.has_uv
&& let Some(map) = &m.metal_rough_map
{
let texel = map.sample_grad(s.uv, s.uv_dx, s.uv_dy);
roughness *= texel.y;
metalness *= texel.z;
}
let mut roughness = roughness.clamp(0.045, 1.0);
let metalness = metalness.clamp(0.0, 1.0);
let mut alpha = roughness * roughness;
let clearcoat = m.clearcoat.clamp(0.0, 1.0);
let mut cc_roughness = m.clearcoat_roughness;
let mut cc_alpha = cc_roughness.clamp(0.045, 1.0).powi(2);
let kernel = (0.3 * s.normal_variation).min(0.04);
if kernel > 0.0 {
alpha = (alpha * alpha + kernel).min(1.0).sqrt();
roughness = alpha.sqrt();
cc_alpha = (cc_alpha * cc_alpha + kernel).min(1.0).sqrt();
cc_roughness = cc_alpha.sqrt();
}
let f0 = Vec3::splat(0.04).lerp(base, metalness);
let diffuse = base * (1.0 - metalness);
let mut direct = Vec3::ZERO;
for light in &ctx.lights.direct {
let (l, radiance) = match &light.kind {
LightKind::Directional { direction, shadow } => {
let l = -*direction;
let mut radiance = light.color;
let n_dot_l = face.dot(l);
if draw.receive_shadow
&& n_dot_l > 0.0
&& let Some(map) = ctx.shadow(*shadow)
{
radiance *= map.visibility(s.position, face, n_dot_l);
}
(l, radiance)
}
LightKind::Point {
position,
distance,
decay,
} => {
let d = *position - s.position;
let dist = d.length();
(
d / dist.max(1e-6),
light.color * attenuation(dist, *distance, *decay),
)
}
LightKind::Spot {
position,
direction,
distance,
decay,
cos_outer,
cos_inner,
} => {
let d = *position - s.position;
let dist = d.length();
let l = d / dist.max(1e-6);
let cone = smoothstep(*cos_outer, *cos_inner, (-l).dot(*direction));
(l, light.color * attenuation(dist, *distance, *decay) * cone)
}
};
let n_dot_l = n.dot(l);
if n_dot_l <= 0.0 || radiance.max_element() <= 0.0 {
continue;
}
let h = (l + v).normalize_or(n);
let n_dot_h = n.dot(h).max(0.0);
let v_dot_h = v.dot(h).max(0.0);
let fresnel = f0 + (Vec3::ONE - f0) * (1.0 - v_dot_h).powi(5);
let specular = fresnel * (ggx(n_dot_h, alpha) * smith_visibility(n_dot_l, n_dot_v, alpha));
let mut lit = (diffuse * (Vec3::ONE - fresnel) / PI + specular) * radiance * n_dot_l;
if clearcoat > 0.0 {
let fc = (0.04 + 0.96 * (1.0 - v_dot_h).powi(5)) * clearcoat;
let coat = ggx(n_dot_h, cc_alpha) * smith_visibility(n_dot_l, n_dot_v, cc_alpha) * fc;
lit = lit * (1.0 - fc) + radiance * (coat * n_dot_l);
}
direct += lit;
}
let mut irradiance = ctx.lights.ambient / PI;
if let Some((sky, ground)) = ctx.lights.hemisphere {
irradiance += ground.lerp(sky, n.y * 0.5 + 0.5) / PI;
}
let env = ctx.environment;
let has_env = !matches!(env, Environment::None);
if has_env {
irradiance += env.irradiance(n) * m.env_intensity;
}
let mut indirect = diffuse * irradiance;
let occlusion = match &m.ao_map {
Some(map) if draw.has_uv => map.sample_grad(s.uv, s.uv_dx, s.uv_dy).x,
_ => 1.0,
};
if has_env {
let mirror = (-v).reflect(n);
let r = mirror.lerp(n, alpha).normalize_or(n);
let horizon = (1.0 + r.dot(face)).clamp(0.0, 1.0).powi(2);
let (scale, bias) = env_brdf(roughness, n_dot_v);
let reflection = ctx.reflection.filter(|_| m.reflection > 0.0);
let uv = s.screen / ctx.viewport;
let env = ctx.env_radiance(r, roughness) * m.env_intensity;
let mut radiance = env * horizon;
if let Some(map) = reflection {
let p = map.sample(uv, roughness);
radiance = radiance.lerp((p.truncate() + env * (1.0 - p.w)) * horizon, m.reflection);
}
indirect += radiance * (f0 * scale + Vec3::splat(bias));
if clearcoat > 0.0 {
let (cs, cb) = env_brdf(cc_roughness, n_dot_v);
let fc = (0.04 * cs + cb) * clearcoat;
let mut coat = ctx.env_radiance(mirror, cc_roughness) * m.env_intensity;
if let Some(map) = reflection {
let p = map.sample(uv, cc_roughness);
coat = coat.lerp(p.truncate() + coat * (1.0 - p.w), m.reflection);
}
indirect = indirect * (1.0 - fc) + coat * (horizon * fc);
}
} else {
let (scale, bias) = env_brdf(roughness, n_dot_v);
indirect += irradiance * (f0 * scale + Vec3::splat(bias)) * 0.5;
}
direct + indirect * occlusion
}
pub(crate) fn attenuation(distance: f32, cutoff: f32, decay: f32) -> f32 {
let d = distance.max(0.01);
let mut att = 1.0 / if decay == 2.0 { d * d } else { d.powf(decay) };
if cutoff > 0.0 {
let x = (1.0 - (distance / cutoff).powi(4)).clamp(0.0, 1.0);
att *= x * x;
}
att
}
#[inline]
fn ggx(n_dot_h: f32, alpha: f32) -> f32 {
let a2 = alpha * alpha;
let d = n_dot_h * n_dot_h * (a2 - 1.0) + 1.0;
a2 / (PI * d * d)
}
#[inline]
fn smith_visibility(n_dot_l: f32, n_dot_v: f32, alpha: f32) -> f32 {
let a2 = alpha * alpha;
let gv = n_dot_l * (n_dot_v * n_dot_v * (1.0 - a2) + a2).sqrt();
let gl = n_dot_v * (n_dot_l * n_dot_l * (1.0 - a2) + a2).sqrt();
0.5 / (gv + gl).max(1e-6)
}
#[inline]
fn env_brdf(roughness: f32, n_dot_v: f32) -> (f32, f32) {
let c0 = Vec4::new(-1.0, -0.0275, -0.572, 0.022);
let c1 = Vec4::new(1.0, 0.0425, 1.04, -0.04);
let r = c0 * roughness + c1;
let a004 = (r.x * r.x).min((-9.28 * n_dot_v).exp2()) * r.x + r.y;
(a004 * -1.04 + r.z, a004 * 1.04 + r.w)
}