readSurface function
Surface?
readSurface(
- Float32List v,
- ShaderBindings bindings,
- FragmentContext c, {
- bool transformed = false,
ReadSurface from surface.glsl.
Null is the GLSL's discard: a masked material's fragment under its
cutoff. Every model that calls this hands the null on, the way a discarded
fragment reaches no lighting loop. Transcribed once the picking stage
needed the same hole — an id pass that saw a fence where the picture shows
the thing behind it would be picking something the eye cannot see — and a
scene pass without it would then have disagreed with the pick.
transformed is the layered stage's MapUv — see mapUv.
Implementation
Surface? readSurface(
Float32List v,
ShaderBindings bindings,
FragmentContext c, {
bool transformed = false,
}) {
final (u: mapU, v: mapV, footprint: uv) = mapUv(
kMapBaseColor,
v,
bindings,
c,
transformed: transformed,
);
final tint = bindings.vec4('FragInfo', 'base_color', Vector4(1, 1, 1, 1));
final texture = bindings.textures['base_color_texture'];
final texel = texture == null
? Vector4(1, 1, 1, 1)
: texture.sample(
mapU,
mapV,
du: uv.du,
dv: uv.dv,
// The albedo is the one map a floor's checks live in, and the one
// this engine's samplers ever ask taps for. The other maps below
// pass the axis maxima alone, which is what they always did.
dudx: uv.dudx,
dvdx: uv.dvdx,
dudy: uv.dudy,
dvdy: uv.dvdy,
);
// Texture and tint are sRGB; the vertex colour is authored linear, per glTF.
final albedo = Vector3(
toLinear(texel.x) * toLinear(tint.x) * v[kVColour],
toLinear(texel.y) * toLinear(tint.y) * v[kVColour + 1],
toLinear(texel.z) * toLinear(tint.z) * v[kVColour + 2],
);
final textured = texel.w * tint.w * v[kVColour + 3];
// Alpha masking, glTF's third alpha mode, before anything else for the
// reason the GLSL gives: a discarded fragment should not pay for the
// lighting loop. A negative cutoff is "not masked", the encoding the engine
// writes into `material2.x`.
final cutoff = bindings
.vec4('FragInfo', 'material2', Vector4(-1.0, 1.0, 1.0, 1.0))
.x;
if (cutoff > 1.0) {
// `P7`'s coverage, which never reaches this rasteriser: it answers false
// to `supportsAlphaToCoverage`, so the engine writes the plain cutoff.
// Cut at the cutoff it carries rather than keep a fragment that one
// sample a pixel has no coverage to spread.
if (textured < cutoff - 1.0) return null;
} else if (cutoff >= 0.0) {
if (textured < cutoff) return null;
} else if (cutoff < -1.5) {
// `gfx-16n`: hashed, the fourth mode. Anchored to the world rather than
// to the screen so the pattern travels with the surface, and stays put
// through a shift of the origin — see [hashedAlphaNoise].
final noise = hashedAlphaNoise(
v[kVWorld],
v[kVWorld + 1],
v[kVWorld + 2],
cutoff,
);
if (textured < noise) return null;
}
// What survives a mask's cut is opaque, as `surface.glsl` writes it.
final alpha = cutoff >= 0.0 && cutoff <= 1.0 ? 1.0 : textured;
final normal = Vector3(v[kVNormal], v[kVNormal + 1], v[kVNormal + 2]);
final length = normal.length;
if (length > 1e-6) normal.scale(1.0 / length);
// `if (!gl_FrontFacing) s.n = -s.n;`: the back of a double-sided surface
// is lit from its own side.
if (!c.frontFacing) normal.negate();
final material = bindings.vec4('FragInfo', 'material', Vector4.zero());
final camera = bindings.vec4('FragInfo', 'camera_position', Vector4.zero());
final world = Vector3(v[kVWorld], v[kVWorld + 1], v[kVWorld + 2]);
// `P7`: against the view axis through an orthographic lens — `TowardsEye`.
// The stages without the fog block keep the eye's point, as the GLSL's
// `F3D_NO_FOG` ones do, and read as perspective here.
final view = orthographic(bindings)
? towardsEye(v, bindings)
: Vector3(camera.x, camera.y, camera.z) - world;
final viewLength = view.length;
if (viewLength > 1e-6) view.scale(1.0 / viewLength);
// Hemispheric, blended on the geometric normal before any map perturbs it:
// ambient of this kind says which half of the world a face can see, and
// millimetres of bump relief are not an answer to that.
final sky = bindings.vec4('FragInfo', 'ambient_sky', Vector4(1, 1, 1, 1));
final ground = bindings.vec4(
'FragInfo',
'ambient_ground',
Vector4(1, 1, 1, 1),
);
final up = normal.y * 0.5 + 0.5;
final ambient = Vector3(
(ground.x + (sky.x - ground.x) * up) * material.z,
(ground.y + (sky.y - ground.y) * up) * material.z,
(ground.z + (sky.z - ground.z) * up) * material.z,
);
// `L5`: what `g_albedo` carries into the albedo buffer, sRGB-encoded as
// `WriteSurfaceGeometry` stores it.
c.albedo = Vector4(
toSrgb(albedo.x.clamp(0.0, 1.0)),
toSrgb(albedo.y.clamp(0.0, 1.0)),
toSrgb(albedo.z.clamp(0.0, 1.0)),
1.0,
);
return Surface(
albedo,
alpha,
normal,
world,
ambient,
material.x.clamp(0.0, 1.0),
material.y.clamp(0.02, 1.0),
view,
math.max(normal.dot(view), 1e-4),
Vector4(v[kVTangent], v[kVTangent + 1], v[kVTangent + 2], v[kVTangent + 3]),
);
}