Build the per-pixel surface sample from a BVH hit. Interpolates barycentric attributes, samples material textures, and perturbs the shading normal via the tangent-space normal map (if present).
(scene: &Scene, ray: &Ray, hit: &Hit)
| 1118 | /// barycentric attributes, samples material textures, and perturbs the |
| 1119 | /// shading normal via the tangent-space normal map (if present). |
| 1120 | fn surface_from_hit(scene: &Scene, ray: &Ray, hit: &Hit) -> SurfaceSample { |
| 1121 | let tri = &scene.triangles[hit.triangle_index as usize]; |
| 1122 | let u = hit.barycentric.x; |
| 1123 | let v = hit.barycentric.y; |
| 1124 | let w = 1.0 - u - v; |
| 1125 | |
| 1126 | let geom_normal = (tri.n0 * w + tri.n1 * u + tri.n2 * v).normalize_or_zero(); |
| 1127 | let uv = tri.uv0 * w + tri.uv1 * u + tri.uv2 * v; |
| 1128 | let position = ray.origin + ray.direction * hit.t; |
| 1129 | |
| 1130 | let material = &scene.materials[tri.material_index as usize]; |
| 1131 | let base_color = scene.sample_base_color(material, uv); |
| 1132 | let (metallic, roughness) = scene.sample_metallic_roughness(material, uv); |
| 1133 | let emissive = scene.sample_emissive(material, uv); |
| 1134 | let occlusion = scene.sample_occlusion(material, uv); |
| 1135 | |
| 1136 | // Build the per-hit TBN from the interpolated tangent+normal. The |
| 1137 | // bitangent sign comes from the glTF tangent.w (±1) — if the mesh |
| 1138 | // has no tangents (length 0), skip normal mapping entirely. |
| 1139 | let tangent_interp = tri.t0 * w + tri.t1 * u + tri.t2 * v; |
| 1140 | let tangent_xyz = Vec3::new(tangent_interp.x, tangent_interp.y, tangent_interp.z); |
| 1141 | let shading_normal = if material.normal_texture.is_some() |
| 1142 | && tangent_xyz.length_squared() > 1e-8 |
| 1143 | { |
| 1144 | let t = tangent_xyz.normalize(); |
| 1145 | // Re-orthogonalize the tangent against the normal (Gram-Schmidt) |
| 1146 | // so numerical drift from interpolation doesn't skew the basis. |
| 1147 | let t = (t - geom_normal * geom_normal.dot(t)).normalize_or_zero(); |
| 1148 | let bitangent_sign = tangent_interp.w.signum().max(-1.0).min(1.0); |
| 1149 | let b = geom_normal.cross(t) * bitangent_sign; |
| 1150 | |
| 1151 | let n_tangent = scene.sample_tangent_normal(material, uv); |
| 1152 | // Compose tangent-space normal into world space. |
| 1153 | (t * n_tangent.x + b * n_tangent.y + geom_normal * n_tangent.z).normalize_or_zero() |
| 1154 | } else { |
| 1155 | geom_normal |
| 1156 | }; |
| 1157 | |
| 1158 | SurfaceSample { |
| 1159 | position, |
| 1160 | normal: shading_normal, |
| 1161 | base_color, |
| 1162 | metallic, |
| 1163 | roughness: roughness.max(0.04), // clamp to avoid div-by-zero |
| 1164 | emissive, |
| 1165 | occlusion, |
| 1166 | } |
| 1167 | } |
| 1168 | |
| 1169 | /// Build an orthonormal basis (tangent, bitangent) around `n`. |
| 1170 | /// Branchless method from Frisvad 2012 — more stable than cross(n, up) |
no test coverage detected