bind method
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,
),
);
}