Skip to content

How model material alpha rendering works

Model materials are now split into three classes by the glTF/generic material alpha contract:

Class GPU state Fragment behavior Order
OPAQUE No blending, depth write Ignores the sampled alpha and outputs 1 AFTER_ENTITIES priority 0
MASK No blending, depth write Fragments below the material alphaCutoff are discarded, the rest output 1 AFTER_ENTITIES priority 1
BLEND OIT accumulation/revealage, no depth write Discards only the near-invisible alpha <= 1/255 and keeps the actual alpha AFTER_TRANSLUCENT_BLOCKS, WBOIT needs no sorting

BackendInstance creates a separate FlatModelData and vertex buffer for each non-empty pass, so a model with several materials does not draw incompatible geometry repeatedly into the wrong RenderType. OPAQUE and MASK can still go into the global batch, but the batch key includes the alpha pass; BLEND does not go into the global batch, which keeps the two OIT geometry submissions apart from ordinary batching state.

The glTF alphaMode and alphaCutoff reach the backend through GltfMaterialData. An older loader with no alpha contract defaults to OPAQUE; the PMX loader infers OPAQUE, MASK, or BLEND from the diffuse alpha and the texture alpha.

The non-Iris path for BLEND is weighted-blended OIT (WBOIT) with two geometry sub-passes: the first adds color * alpha * weight and alpha * weight into an RGBA16F accumulation, and the second multiplies 1 - alpha into an R16F revealage. Both passes use the scene depth copied from the main framebuffer and test depth without writing it; a final fullscreen resolve computes color = accumulation.rgb / accumulation.a and alpha = 1 - revealage, then composites back into the main target with straight-alpha source-over. KasugaLib's BLEND geometry therefore does not depend on far-to-near submission order on this path.

With an Iris shader pack enabled the same accumulation/revealage/resolve targets are used, but Iris's entities_translucent shader is kept to produce the straight-alpha color after materials, PBR, lighting, and fog. Since Kasuga's depth weights cannot be injected into its fragment shader without changing the shader pack, the Iris path uses constant-weight WBOIT with a weight of 1: RGB accumulates with SRC_ALPHA, ONE, A accumulates with ONE, ONE, and revealage stays ZERO, ONE_MINUS_SRC_ALPHA. The translucent entity shader of Iris 1.8.12 brings its own 0.1 alpha test, below the 0.5 cutoff MASK usually uses.

Iris binds the shader pack gbuffer in every ShaderInstance.apply() and may override blend state, so Kasuga rebinds the OIT attachment after apply and before draw, and explicitly restores the blend equation, blend factors, and color/depth masks. The scene depth is copied from Iris's after-translucent default framebuffer, and the resolve writes back into the same framebuffer; only the dedicated OIT composite shader may bypass the Iris unknown-shader guard, and the compatibility policy for other mod shaders is not loosened.

WBOIT is an approximation, not exact arbitrary-layer source-over, and it does not fold vanilla water or shader pack translucent fragments into the same OIT equation; the stage is still AFTER_TRANSLUCENT_BLOCKS, so its scope is "the KasugaLib BLEND layer on top of an already finished scene". When a floating-point target is unavailable, the Iris pipeline has no bindable world framebuffer, or a runtime framebuffer operation fails, it falls back safely to the old far-to-near sorting path. Iris OIT resolves only the main color, so a shader pack that expresses translucent materials through custom extra gbuffer channels may still need pack-specific integration.

The material alphaCutoff of MASK and the near-zero 1/255 working cutoff of BLEND are two separate concepts: the first decides whether depth is written, and the second only removes nearly invisible OIT work items.

For debug commands, keybinds, and the acceptance entry see the API reference.