Material evaluation and shader variants
Examine material-table indirection, texture filtering, relief sampling, shared appearance rows, and shader specialization.
Asterune separates surface evaluation from light evaluation. Surface code produces color, normal, roughness, metalness, occlusion, emission, and additional layer parameters. Lighting consumes those results. A material can therefore change how a surface is sampled without replacing the entire world-lighting pipeline.
The renderer combines table-driven material parameters with specialized GPU pipelines. Table rows let many surfaces share state. Pipeline specialization removes code that a frame or draw cannot use. These optimizations act at different levels: sharing a row does not establish that 2 meshes can share a draw command.
Material-table indirection
A material word identifies a built-in material and its selected set or variant. The renderer uploads GPU material rows containing texture-layer indices, feature flags, tiling, surface parameters, and layer settings. A shader reads the selected row rather than binding a separate uniform buffer for every material property.
Each material set occupies a stride of 256 rows. An additional pool of 1,024 rows holds named variants, per-part appearances, shader parameters, and related runtime state. Named variants retain their name and material identity; appearance rows use their serialized values and associated variant as a sharing key.
Identical appearances on different parts can therefore use the same row. Animating an appearance can request a different row as its values change. The renderer releases rows for appearances absent from the live set and reuses those slots before growing the pool. Without reclamation, a continuously changing value could consume the finite table even with few visible parts. A full pool returns slot 0 instead of extending the GPU table without a bound.
A slot-generation counter changes when rows are allocated or returned. The material upload cache includes that generation alongside the live variant and appearance descriptions. Reusing a slot therefore invalidates data that belonged to an earlier assignment.
Texture arrays and channel interpretation
Material packs place texture layers into arrays, allowing a row to select a layer without replacing the shader's texture bindings. Color textures use an sRGB format; surface and normal data use linear formats. Packed surface RGB holds occlusion, roughness, and metalness. Treating those values as display color would apply a transfer curve to numeric material data.
When block compression is available, pack color and surface maps use BC1, normal maps use BC3, and height maps use BC4. The portable path uploads decoded texels instead. Pack color, normal, and surface layers use a 512-pixel base size with 10 mip levels. Height layers use half that size and one fewer level. The renderer bounds the layer set differently for compressed and uncompressed storage because their memory costs differ.
Packed normals store 2 axes in channels preserved by the compression format.
The shader reconstructs the remaining axis with
sqrt(max(1 - x² - y², 0)). Detail normals add a higher-frequency contribution
before the final shading normal is constructed. Its strength fades with view
distance so distant detail does not retain the same normal variation as a
near surface.
Mip filtering and coverage preservation
Mip generation distinguishes color, linear data, and normals. The shared filter uses a windowed sinc with 3 lobes and retains floating-point values between successive levels. Color is decoded to linear light before filtering and encoded again afterward. Linear material channels do not receive that color conversion.
Loose normal maps average vectors without normalizing away their shortened length. Disagreement among source normals reduces that length, preserving a signal that material shading uses for specular anti-aliasing. Renormalizing every mip would remove that information. The image-space and specular paths are compared in Anti-aliasing and render resolution.
Cutout color textures preserve the share of texels passing a 0.5 alpha test. The generator estimates source coverage and rescales alpha in each smaller level toward that coverage. Otherwise, filtering a thin opaque feature with transparent neighbors could make the whole feature fall below the cutoff. This filter preserves texture coverage; rasterized geometric coverage still depends on the selected anti-aliasing path.
Engine atlases use a separate repeated box reduction to limit bleeding between adjacent cells. A texture's intended interpretation is therefore part of its filtering input, rather than something inferred only from the filename.
The loose-texture mip cache hashes source pixels, dimensions, interpretation, and a filtering-version marker. It stores the smaller levels compressed on disk while retaining level 0 with the decoded source. A cache miss or invalid entry regenerates the chain. A changed normal interpretation cannot reuse the same cache entry merely because the original pixel bytes match.
Relief and repeated-surface sampling
Pack sampling derives UVs and explicit gradients from the surface projection. Those gradients select the texture footprint while additional sampling changes the UV position. This avoids treating a procedural offset as an unrelated screen derivative.
Relief sampling marches a view ray through the height texture. The layer count varies with ray steepness, stays within a 64-step bound, and uses the quality profile's step budget. After crossing the height field, the shader interpolates between the preceding and current UV positions. It changes where the surface samples its material rather than creating collision geometry or a displaced mesh silhouette.
The effect fades between 60 and 120 studs of view distance. Axis-projected terrain uses a fixed projection frame for its ray; other surfaces construct a tangent frame from position and UV gradients. Terrain also fades relief near grazing directions. Using a hillside's derivative tangent for a fixed-axis projection would shift the texture as camera height changes.
Tiling breakup blends 2 noise-selected UV offsets and can mix that result with the original sample. Far tiling separately blends toward another sampling frequency as distance increases. These paths add samples to reduce visible repetition; they do not change how many geometry instances the renderer submits.
Frame and draw specialization
A world-shader key records features proven unused by the frame, including light families, shadow paths, screen-space effects, and atmosphere state. Per-draw facts further remove material maps, layers, transmission, or scripts absent from that draw. The key also identifies the shader family and the generated surface-script set.
Specialization replaces constant masks in the shader source, letting the GPU compiler remove unreachable branches. Merely leaving a branch untaken at runtime can retain its register requirements. A specialized pipeline reduces that requirement before the draw executes.
The asynchronous path waits until a key has been requested across 20 view updates before queuing compilation. A single worker builds variants. Completed pipelines are adopted at view boundaries, keeping choices stable while render bundles are recorded.
A draw first tries its own specialized pipeline, then the frame's broader variant, then an always-available general pipeline. A failed key stays on the fallback rather than repeatedly compiling during every draw. Changes in selected pipelines are also part of opaque-bundle cache invalidation.
Surface shader integration
A surface shader defines the engine's surface function and receives the
prepared material values. It runs inside the world shader, before the resulting
material enters lighting. A full-screen ShaderEffect instead reads a display
image later in the post-processing chain.
The renderer validates surface source by inserting it into the complete world module and parsing and validating that module with Naga. Error locations are mapped back to the supplied source. Validation therefore checks the function in the shader context where it executes, rather than accepting a standalone snippet whose surrounding types differ.
Source cannot declare resource bindings, GPU entry points, or enable and
requires directives. The engine owns those declarations. The declaration
check tokenizes source so matching words inside comments do not become false
rejections. Source length is bounded to 64 KiB.
Accepted sources receive an index from a registry with capacity for 255 distinct surface shaders. Index 0 selects the base behavior. Generated code renames each shader's declarations and dispatches by the index stored in the material row, preventing helper-name collisions between accepted sources.
The generated script set has its own source hash. Variants include that hash in their key, so adding a source changes the compiled module identity. Parameter and clock rows supply values without requiring a different source for each animated value. Identical source can reuse its registered index; rejected sources cache their validation result.
MaterialService, MaterialVariant, and SurfaceAppearance describe the controls that feed these paths.