bind method

  1. @override
void bind(
  1. RenderPass pass,
  2. HostBuffer transientsBuffer,
  3. Lighting lighting
)
override

Binds this material's render-pass state, uniforms, and textures.

The base implementation enables back-face culling with counter-clockwise winding (matching the glTF convention). Subclasses must call super.bind and then bind any per-material uniforms and textures expected by their fragment shader. lighting carries the IBL EnvironmentMap (and its intensity) plus the analytic lights and shadow resources that materials shade against.

Implementation

@override
void bind(
  gpu.RenderPass pass,
  gpu.HostBuffer transientsBuffer,
  Lighting lighting,
) {
  super.bind(pass, transientsBuffer, lighting);

  final EnvironmentMap env = environment ?? lighting.environmentMap;
  final DirectionalLight? light = lighting.directionalLight;
  final cascades =
      lighting.shadowMap == null
          ? const <ShadowCascade>[]
          : lighting.cascades;

  // FragInfo std140 layout (608 bytes / 152 floats):
  //   [0..3]    vec4  color
  //   [4..7]    vec4  emissive_factor
  //   [8..43]   vec4  diffuse_sh0..8 (xyz used, w padding)
  //   [44..47]  vec4  directional_light_direction (xyz used)
  //   [48..51]  vec4  directional_light_color (rgb = color * intensity)
  //   [52..115] mat4  light_space_matrix[4] (shadow cascades)
  //   [116..119] vec4 cascade_box_sizes (world-space box size each)
  //   [120]     float vertex_color_weight
  //   [121]     float metallic_factor
  //   [122]     float roughness_factor
  //   [123]     float has_normal_map
  //   [124]     float normal_scale
  //   [125]     float occlusion_strength
  //   [126]     float environment_intensity
  //   [127]     float has_directional_light
  //   [128]     float casts_shadow
  //   [129]     float shadow_bias
  //   [130]     float shadow_normal_bias
  //   [131]     float shadow_texel_size (1 / cascade tile resolution)
  //   [132]     float render_target_flip_y
  //   [133]     float alpha_mode (0 opaque, 1 mask, 2 blend)
  //   [134]     float alpha_cutoff
  //   [135]     float shadow_fade (world-space far-edge fade width)
  //   [136]     float shadow_softness (world-space penumbra radius)
  //   [137]     float shadow_cascade_count
  //   [138..139] padding to a 16-byte boundary
  //   [140..150] mat3  environment_transform (3 vec3 columns, w padding)
  final fragInfo = Float32List(152);
  fragInfo[0] = baseColorFactor.r;
  fragInfo[1] = baseColorFactor.g;
  fragInfo[2] = baseColorFactor.b;
  fragInfo[3] = baseColorFactor.a;
  fragInfo[4] = emissiveFactor.r;
  fragInfo[5] = emissiveFactor.g;
  fragInfo[6] = emissiveFactor.b;
  fragInfo[7] = emissiveFactor.a;
  final shCoefficients = env.diffuseSphericalHarmonics;
  for (var i = 0; i < shCoefficients.length; i++) {
    fragInfo[8 + i * 4] = shCoefficients[i].x;
    fragInfo[9 + i * 4] = shCoefficients[i].y;
    fragInfo[10 + i * 4] = shCoefficients[i].z;
  }
  if (light != null) {
    fragInfo[44] = light.direction.x;
    fragInfo[45] = light.direction.y;
    fragInfo[46] = light.direction.z;
    fragInfo[48] = light.color.x * light.intensity;
    fragInfo[49] = light.color.y * light.intensity;
    fragInfo[50] = light.color.z * light.intensity;
  }
  for (var i = 0; i < cascades.length; i++) {
    fragInfo.setRange(
      52 + i * 16,
      68 + i * 16,
      cascades[i].lightSpaceMatrix.storage,
    );
    fragInfo[116 + i] = cascades[i].boxSize;
  }
  fragInfo[120] = vertexColorWeight;
  fragInfo[121] = metallicFactor;
  fragInfo[122] = roughnessFactor;
  fragInfo[123] = normalTexture != null ? 1.0 : 0.0;
  fragInfo[124] = normalScale;
  fragInfo[125] = occlusionStrength;
  fragInfo[126] = lighting.environmentIntensity;
  fragInfo[127] = light != null ? 1.0 : 0.0;
  fragInfo[128] = cascades.isEmpty ? 0.0 : 1.0;
  fragInfo[129] = light?.shadowDepthBias ?? 0.0;
  fragInfo[130] = light?.shadowNormalBias ?? 0.0;
  fragInfo[131] = light == null ? 0.0 : 1.0 / light.shadowMapResolution;
  // Render-to-texture targets (the shadow map, the prefiltered-radiance
  // atlas) sample top-down on Metal/Vulkan and bottom-up on OpenGL ES.
  // Flutter GPU has no backend query; offscreen-MSAA support is a proxy
  // (true on Metal/Vulkan, false on OpenGL ES).
  fragInfo[132] = gpu.gpuContext.doesSupportOffscreenMSAA ? 1.0 : 0.0;
  fragInfo[133] = alphaMode.index.toDouble();
  fragInfo[134] = alphaCutoff;
  fragInfo[135] = light?.shadowFadeRange ?? 0.0;
  fragInfo[136] = light?.shadowSoftness ?? 0.0;
  fragInfo[137] = cascades.length.toDouble();
  // mat3 environment_transform: std140 stores each column as a vec3
  // padded to 16 bytes, so the three columns land at [140], [144],
  // [148]. Matrix3.storage is column-major (3 floats per column).
  final envTransform = lighting.environmentTransform.storage;
  for (var col = 0; col < 3; col++) {
    fragInfo[140 + col * 4] = envTransform[col * 3];
    fragInfo[141 + col * 4] = envTransform[col * 3 + 1];
    fragInfo[142 + col * 4] = envTransform[col * 3 + 2];
  }
  pass.bindUniform(
    fragmentShader.getUniformSlot("FragInfo"),
    transientsBuffer.emplace(ByteData.sublistView(fragInfo)),
  );

  pass.bindTexture(
    fragmentShader.getUniformSlot('base_color_texture'),
    Material.whitePlaceholder(baseColorTexture),
    sampler: gpu.SamplerOptions(
      widthAddressMode: gpu.SamplerAddressMode.repeat,
      heightAddressMode: gpu.SamplerAddressMode.repeat,
    ),
  );
  pass.bindTexture(
    fragmentShader.getUniformSlot('emissive_texture'),
    Material.whitePlaceholder(emissiveTexture),
    sampler: gpu.SamplerOptions(
      widthAddressMode: gpu.SamplerAddressMode.repeat,
      heightAddressMode: gpu.SamplerAddressMode.repeat,
    ),
  );
  pass.bindTexture(
    fragmentShader.getUniformSlot('metallic_roughness_texture'),
    Material.whitePlaceholder(metallicRoughnessTexture),
    sampler: gpu.SamplerOptions(
      widthAddressMode: gpu.SamplerAddressMode.repeat,
      heightAddressMode: gpu.SamplerAddressMode.repeat,
    ),
  );
  pass.bindTexture(
    fragmentShader.getUniformSlot('normal_texture'),
    Material.normalPlaceholder(normalTexture),
    sampler: gpu.SamplerOptions(
      widthAddressMode: gpu.SamplerAddressMode.repeat,
      heightAddressMode: gpu.SamplerAddressMode.repeat,
    ),
  );
  pass.bindTexture(
    fragmentShader.getUniformSlot('occlusion_texture'),
    Material.whitePlaceholder(occlusionTexture),
    sampler: gpu.SamplerOptions(
      widthAddressMode: gpu.SamplerAddressMode.repeat,
      heightAddressMode: gpu.SamplerAddressMode.repeat,
    ),
  );
  // Specular IBL atlas: horizontal repeat (the panorama wraps in
  // longitude), vertical clamp (it's a stack of roughness bands;
  // wrapping V would bleed between them).
  pass.bindTexture(
    fragmentShader.getUniformSlot('prefiltered_radiance'),
    env.prefilteredRadianceTexture,
    sampler: gpu.SamplerOptions(
      minFilter: gpu.MinMagFilter.linear,
      magFilter: gpu.MinMagFilter.linear,
      widthAddressMode: gpu.SamplerAddressMode.repeat,
      heightAddressMode: gpu.SamplerAddressMode.clampToEdge,
    ),
  );
  pass.bindTexture(
    fragmentShader.getUniformSlot('brdf_lut'),
    Material.getBrdfLutTexture(),
    sampler: gpu.SamplerOptions(
      minFilter: gpu.MinMagFilter.linear,
      magFilter: gpu.MinMagFilter.linear,
      widthAddressMode: gpu.SamplerAddressMode.clampToEdge,
      heightAddressMode: gpu.SamplerAddressMode.clampToEdge,
    ),
  );
  // Bilinear + clamp. Linear filtering interpolates the stored depth
  // between texels, so a flat receiver compares against the smooth
  // surface rather than a coarse cascade's blocky per-texel depth,
  // which removes the patchy self-shadow on distant ground. Clamp (not
  // wrap) keeps out-of-bounds PCF taps from reading another cascade's
  // tile. When there's no shadow this frame the white placeholder
  // reads as depth 1.0 (always lit) and casts_shadow is 0 anyway.
  pass.bindTexture(
    fragmentShader.getUniformSlot('shadow_map'),
    Material.whitePlaceholder(lighting.shadowMap),
    sampler: gpu.SamplerOptions(
      minFilter: gpu.MinMagFilter.linear,
      magFilter: gpu.MinMagFilter.linear,
    ),
  );
}