applyNormalMap function

void applyNormalMap(
  1. Surface s,
  2. Float32List v,
  3. ShaderBindings b,
  4. FragmentContext c, {
  5. 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);
}