applyNormalMap function
void
applyNormalMap(
- Surface s,
- Float32List v,
- ShaderBindings b,
- FragmentContext c, {
- bool transformed = false,
Perturbs the normal by the tangent-space map.
Implementation
void applyNormalMap(
Surface s,
Float32List v,
ShaderBindings b,
FragmentContext c, {
bool transformed = false,
}) {
final map = b.textures['normal_texture'];
if (map == null) return;
// Re-orthogonalised against the normal: interpolating both across a triangle
// does not preserve the right angle between them.
final t = Vector3(s.tangent.x, s.tangent.y, s.tangent.z);
t.sub(s.normal * s.normal.dot(t));
if (t.length2 < 1e-12) return; // no usable frame; keep the vertex normal
t.normalize();
// The bitangent sign encodes a mirrored UV island. Dropping it lights every
// mirrored half of a symmetric model from the wrong side.
final bitangent = s.normal.cross(t)..scale(s.tangent.w);
// `C8`: the frame turns with a map its transform turns or mirrors, as
// `ApplyNormalMap` turns it, on the front face's frame. dP/dv is minus the
// bitangent, so `m10` adds it.
final rows = transformed ? b.read('LayerInfo', 'uv_transform') : null;
if (rows != null && rows.length >= kMapNormal * 8 + 8) {
const o = kMapNormal * 8;
final (m00, m01, m10, m11) = (
rows[o],
rows[o + 1],
rows[o + 4],
rows[o + 5],
);
final flip = m00 * m11 - m01 * m10 < 0.0 ? -1.0 : 1.0;
final front = c.frontFacing ? bitangent : -bitangent;
final turned = (t * m11 + front * m10)..scale(flip);
if ((m01 != 0.0 || m10 != 0.0 || m00 < 0.0 || m11 < 0.0) &&
turned.length2 > 1e-12) {
t.setFrom(turned..normalize());
bitangent.setFrom(s.normal.cross(t)..scale(s.tangent.w * flip));
}
}
// The back face turns the whole frame: the normal and the bitangent built
// from it have turned already, the tangent follows — see
// `material_maps.glsl`.
if (!c.frontFacing) t.negate();
final (u: mu, v: mv, footprint: uv) = mapUv(
kMapNormal,
v,
b,
c,
transformed: transformed,
);
final texel = map.sample(mu, mv, du: uv.du, dv: uv.dv);
final scale = b.vec4('FragInfo', 'material2', Vector4.zero()).y;
final x = texel.x * 2.0 - 1.0;
final y = texel.y * 2.0 - 1.0;
// A two-channel map rebuilds z from the unit length, before the scale —
// `emissive.w`, as `ApplyNormalMap` reads it.
final sz = b.vec4('FragInfo', 'emissive', Vector4.zero()).w > 0.5
? math.sqrt(math.max(1.0 - x * x - y * y, 0.0))
: texel.z * 2.0 - 1.0;
final sx = x * scale;
final sy = y * scale;
s.normal = (t * sx + bitangent * sy + s.normal * sz)..normalize();
s.nDotV = math.max(s.normal.dot(s.view), 1e-4);
}