@forgeax/engine-shader
v0.3.11
Published
Runtime shader registry — content-addressable manifest lookup → rhi.createShaderModule. Physically isolated from the build-time triple @forgeax/engine-shader-compiler / @forgeax/engine-naga / @forgeax/engine-wgpu-wasm (AC-06 三重闸门 grep 守护).
Readme
@forgeax/engine-shader
Public shader input boundary
The build manifest is the single published shader input catalog. The plugin
composes engine WGSL modules and material rows once; runtime resolves the
content-addressed result through ShaderRegistry. The shared View module is
identified by forgeax_view::common; its typed ABI describes offsets and
binding metadata only, never a device or GPU handle. Fog and temporal fields
are transported through this existing module/reflection path rather than a
second manifest or registry.
The plugin may publish its versioned, shared-source form.
ShaderRegistry.loadManifest() checks every source digest and expands it before
validating the existing entry and material contracts; failed publication
validation leaves the registry unchanged. The runtime does not compile WGSL.
Digest verification runs in bounded batches; every source still passes SHA-256
before the expanded manifest can be published.
Registries that fetch byte-identical manifest text from the same URL share one
verified expansion, held weakly so it lives only while a registry retains it;
renderer hosts that stay alive together therefore do not each hold a copy of
every composed WGSL source. Changed text is parsed and verified again.
ShaderRegistry.materialProgram(source) admits one immutable program description
per composed source within the registry lifetime. The description owns the source,
its identity, group-2 contract and ProbeBlend requirement. Registrations, selected
variants and material passes share it; forkForDevice() retains these CPU facts
while GPU modules are rebuilt. New source selects a new description. The runtime
MaterialShaderArtifact.program carries this description instead of independent
WGSL, identity and optional binding-decision fields. Synthetic producers use
createMaterialShaderProgram() when constructing an artifact, never while drawing.
The published ray diffuse composite and Standard environment lighting share
standardDiffuseWeight in forgeax_pbr::ibl_shared. The traced input is
unit-receiver D = E/pi; the composite applies the G-buffer receiver color,
metallic/Fresnel response and material occlusion once, then adds only RGB to the
existing linear HDR target. It preserves direct lighting, emission and coverage.
Atmospheric transport
forgeax_atmosphere::optics owns spherical bounds, densities, normalized phases,
solar transmission and segment integration. Sky View, finite-depth aerial
perspective and capture/direct rays share this kernel. Local geometry/cloud
visibility multiplies the single-scattering source only. A separate low-frequency
multiple-scattering LUT supplies the high-order approximation. Radiance LUTs
use unit-white-sun values; consumers apply solar colour and lux once. RGB
transmittance is independent of alpha. The homogeneous far endpoint preserves
view-ray direction at orbital coordinates and supports infinite far planes.
The 1,280-byte View carries six medium vectors and one control vector after the
existing fog lanes. Material capability storage-buffer-atmosphere selects the
cooked atmosphere bindings; the receiving device must admit the full texture
budget. Utility program defines are build-time inputs too. Runtime stays compiler
free, including cloud transport/resolve and custom Surface material programs.
view_fog composes extra height fog over physical AP. Straight alpha uses TC+L,
premultiplied alpha uses TC+alpha*L, and additive uses TC. Transmission keeps
its already-fogged backdrop separate: T*C_local + k*B + (1-k)*L, where k is
the final RGB transmission weight. No second fog pass follows translucency.
LOD coverage ABI
The GPU-driven cull selects the LOD level and signed coverage per view and
writes them into the visible item vec4<u32>: x is the GpuScene instance
row, y the scene material row, z the skin palette base (0 for rigid), and
w the bitcast signed coverage (zero keeps all samples). Scene-index vertex
stages resolve world, previous-world, temporal flags and probe identity through
forgeax_view::common::sceneIndexDraw(visible.x); there is no per-batch Mesh
row. Standard's flat material-address varying carries coverage in its fourth
lane, and ShadowCaster carries it beside the material row. Forward, Deferred,
rigid LOD temporal data, and ShadowCaster call the shared
forgeax_view::common::applyLodCoverage predicate before committing fragments.
The pair uses the same stable pixel-space noise and opposite sides of one
threshold. Encoding the pair as +t / -t keeps that threshold identical
after float32 upload, including near rounding boundaries. This keeps opaque
depth writes and material alpha masks intact.
[!IMPORTANT] Migration: a cooked scene-index material must not read
Meshrows or treatvisible.xas a batch-local index.visible.xis the scene instance row; callsceneIndexDraw(visible.x)and take coverage frombitcast<f32>(visible.w).
MaterialAsset 唯一成功路径
paramSchema -> derive -> compile/reflect -> cook/load -> extract/record
贯穿 shader 与 material。WGSL producer 只声明与 schema 对应的字段;纹理槽
携带 coordinateSet、transform 与 physicalUvScale,cook 后由
layoutIdentity 绑定 shader artifact。identity 失效时修 source 或 cook 输入,
再执行 recook/load。
[!IMPORTANT] Runtime 只查找已发布的 content-addressed artifact;恢复沿 producer、cook 与 catalog 的 owner 边界进行,不在 app 侧复制 shader artifact。
Standard Surface contract
Standard material 的作者只需要通过 #import 使用
surface_v1.wgsl,并实现唯一的
evaluate_surface(SurfaceInput) -> SurfaceData。SurfaceData 是 Standard
BRDF 的输入,不是最终颜色;Engine-owned Standard pass family 负责顶点、光照、
阴影、Forward/Deferred、G-buffer、雾和输出。内建默认值位于
default_standard_surface.wgsl。
Minimal authoring shape
SurfaceInput exposes uv0 through uv7, matching the geometry coordinate
inputs. Every texture slot selects its own set, scale, rotation and offset.
The default Surface evaluator is shared by CPU, GPU scene-index and shadow
passes; no shadow-only sampling formula may discard those authored coordinates.
Changes to the engine-owned input ABI require re-cooking authored materials.
SurfaceData is deliberately base-only. Its frozen field order is:
| Order | Field | Meaning |
|:--:|:--|:--|
| 1 | baseColor: vec3<f32> | Base reflectance color |
| 2 | normalWS: vec3<f32> | World-space shading normal |
| 3 | metallic: f32 | Metallic factor |
| 4 | roughness: f32 | Roughness factor |
| 5 | emissive: vec3<f32> | Emissive contribution |
| 6 | occlusion: f32 | Ambient occlusion factor |
| 7 | opacity: f32 | Surface opacity |
| 8 | alphaClipThreshold: f32 | Alpha-test threshold |
#import forgeax_material::surface_v1::{SurfaceInput, SurfaceData}
fn evaluate_surface(input: SurfaceInput) -> SurfaceData {
return SurfaceData(
vec3<f32>(0.45, 0.18, 0.06), input.vertexNormalWS, 0.8, 0.42,
vec3<f32>(0.0), 1.0, 1.0, 0.5,
);
}The matching TypeScript entry is also import-first: the caller names the compiled Surface module and supplies only its parameters and values. The Engine still owns stage entry points, bindings, BRDF composition, and pass projection.
import { Materials } from '@forgeax/engine-render';
const rustedIron = Materials.standard({
surfaceModule: 'game_3d::rusted_iron_surface',
parameters: [
{ name: 'ironColor', type: 'color' },
{ name: 'rustDark', type: 'color' },
{ name: 'rustBright', type: 'color' },
{ name: 'noiseScale', type: 'f32', default: 1.85 },
],
values: {
ironColor: [0.4, 0.45, 0.47, 1],
rustDark: [0.42, 0.085, 0.018, 1],
rustBright: [0.95, 0.34, 0.055, 1],
noiseScale: 1.85,
},
});For the complete source-to-runtime example, see the
game-3d rusted-iron fixture.
[!CAUTION] Surface is a base-facts function only. Do not add
clearcoat,clearcoatRoughness, other physical-layer fields, stage entries, resource bindings, Engine entry points, or vertex-position mutation. Physical layers and pass admission remain Engine-owned.
渐进导航:Materials.standard() → moduleSlots.surface → build-time
#import composition / reflection → Pack cook → runtime GUID readiness。
Surface source 不声明 stage entry、@group/@binding 或 vertex mutation;参数
资源由同一 MaterialAsset.parameters contract 派生。编译或 cook 失败时读取
code、detail、hint,修复 authored source 或 producer 后重新 cook,不在
runtime 读取 raw WGSL 或创建 app-local artifact。
[!IMPORTANT] A custom material starts as WGSL source plus one
MaterialAssetcontract. The build manifest publishes the composed module and the material cook publishes the resolved record, artifact bytes, references, and receipt. Runtime resolves those facts from the catalog; application code does not install or duplicate shader artifacts. The recovery route is always source or cook repair.
Direct material program contract
A cooked MaterialProgramAbi always names its direct vertex entry. Its optional
sceneIndexEntry exists only when the producer compiled that entry. Direct-only
Unlit programs publish their actual schema-aligned row, reflected vertex inputs
and skin palette address. Cook publishes plain/color and mesh/skinned programs
under the same GUID; Render selects the exact resident geometry contract. Their
absence of a scene entry keeps these programs on the ordinary submission lane.
Scene-index material program contract
An authored full-custom program may opt into the indirect lane only through the
compiler-controlled scene-index contract. Its WGSL must expose both vs_main
and vs_scene_index, import forgeax_material::parameters (or declare an
equivalent producer-owned row), and use the visible-item/material index in the
scene entry. The compiler validates the selected entries and publishes one
MaterialProgramAbi containing the direct/scene-index entries, reflected
resources, vertex inputs, and schema-derived row fields. Custom rows are packed
into the canonical Standard GPU Scene row; schemas larger than that page fail at cook
time and stay on their authored/specialized lane.
Render requests address: 'scene-index' for an indirect draw and
address: 'direct' for the ordinary path. The runtime never reconstructs a
receipt from a shader name and never silently substitutes the direct entry when
the scene-index pair is absent. Engine Standard passes sharing one Surface
automatically
provide that opacity (including Alpha Mask) to their default ShadowCaster.
The compiler publishes its scene-index ABI while preserving authored pass
membership, winding and culling; a program without a shadow pass stays shadowless.
An explicit shadow implementation remains authoritative.
Full-custom materials can select forgeax::default-shadow-caster for native
opaque coverage. The compiler publishes its direct/scene-index pair from the
custom root schema and limits its vertex inputs to position plus any selected
skin attributes; clipping and LOD coverage remain native. Custom deformation or
cutout coverage requires an authored shadow program or explicit Surface; the
compiler never infers those semantics. ShadowCaster is selected independently,
so a custom shadow program may own different coverage while sharing the same
material schema and resource provenance.
For an AI inspection, resolve the published artifact and branch on the structured result before preparing a draw. The renderer-facing adapter selects the concrete pass/address variant internally; the contract is the selected artifact and receipt being one producer publication:
const published = shaderRegistry.findMaterialArtifact(materialShaderId);
if (!published.ok) {
report(published.error.code, published.error.detail, published.error.hint);
return;
}
const receipt = published.value.receipt;
if (receipt === undefined) {
report('material-receipt-missing');
return;
}Application code must not fabricate a receipt from a shader identifier. A
direct-only custom artifact, including the hello/custom-shader fixture,
remains valid for the ordinary path but is intentionally absent from
GPU-driven admission.
Standard lit shader contract
standard-gbuffer.wgsl owns packed material encoding and decoding for rigid,
skinned and custom Standard Surfaces. The deferred attachment contract lives in
Render: geometry
initializes the existing HDR SceneColor with emissive/opacity and writes four
integer material targets. Deferred lighting adds RGB while preserving alpha.
SSAO and SSR decode the shared packed normal before spatial reconstruction.
The Standard lit and skinned PBR shaders consume one
forgeax_standard::cluster::evaluateStandardClusterLights accessor. The
accessor decodes the shared DirectLightSlot and delegates punctual BRDF evaluation;
Forward and Deferred differ only in graph topology. Unlit and fog consumers do
not import this accessor. The canonical source is src/standard-cluster.wgsl;
all Standard lit variants compose this one module, so there is no second
implementation to maintain.
src/standard-surface.wgsl owns the Standard material/fragment evaluation shared
by ordinary Mesh and the VFX Mesh vertex adapter. Geometry adapters supply world
position, normal, tangent, UVs and color; they do not fork BRDF, shadow reception
or IBL formulas. Render supplies the corresponding lighting resources. The
single-level BRDF LUT uses explicit-LOD sampling, including when per-particle
surface factors make fragment control flow non-uniform.
Geometric specular anti-aliasing is part of that shared evaluation, not a material
parameter or render setting. src/specular-aa.wgsl
(forgeax_pbr::specular_aa) widens GGX roughness by the pixel's interpolated-normal
spread: the three.js getGeometryRoughness measure at half weight (UE
NormalCurvatureToRoughness and Tokuyoshi-Kaplanyan filter the same screen
derivatives). Forward direct and IBL lighting, clearcoat, and the roughness
packed into the Deferred G-buffer all consume the filtered value, so both render
paths agree. The spread is sampled at the top of each fragment entry because
derivatives need uniform control flow. Flat surfaces are unchanged bit for bit.
packages/runtime/src/__tests__/specular-aa.dawn.test.ts gates the effect against
an 8x supersampled truth rendered without the filter. It also replays the captured
Deferred G-buffer on a fresh device with and without the filter and records the
GPU pass cost of each path.
Specular IBL conserves rough-metal energy with the table-free multiple-scattering
term of Fdez-Aguera (JCGT 2019), the same form as three.js
computeMultiscattering and UE EnvBRDF energy compensation. The existing split-sum
LUT supplies A/B; specularEnvironmentAlbedo in src/ibl-sampling.wgsl
returns FssEss + Fms·Ems with Ems = 1 − A − B and
Favg = F0 + (1 − F0)/21. Every consumer (Standard Surface, Deferred lighting,
clearcoat, single-layer media, skinned PBR) reaches it through
projectSpecularRadiance; diffuse kD is unchanged. A white metal in a
uniform radiance-one furnace returns one at every roughness, while colored
metals gain only the missing bounce share. Two Dawn gates pin this:
packages/render/src/__tests__/ibl-multiscatter-furnace.dawn.test.ts probes the
kernel against the baked LUT, and packages/runtime/src/__tests__/ibl-furnace.dawn.test.ts
renders roughness 0→1 spheres on both paths. It replays the captured shading draw
with and without the term, and records interleaved GPU pass cost. A single-scatter
build of the same manifest is the falsifier.
MaterialAsset and shader route
The recovery route is to inspect the structured code, detail, and hint, then repair the source or cook input before retrying the catalog load.
Declare passes[].program.module, parameters, values, and optional parent on the same
MaterialAsset. A texture value keeps its own coordinates.set and
coordinates.transform, so every slot remains explicit from glTF import to
fragment sampling. The root contract is inherited by child materials; a child
only supplies values it owns.
assets.configurePackIndex('/pack-index.json');
const result = await assets.loadByGuid<MaterialAsset>(materialGuid);
if (!result.ok) {
report(result.error.code, result.error.detail, result.error.hint);
return;
}
const materialHandle = world.allocSharedRef('MaterialAsset', result.value);The custom-shader demo is the executable reference: apps/hello/custom-shader. It loads the root and derived GUIDs, validates their cooked records, checks the manifest artifact, and only then draws.
GPU-driven PBR / shadow / skin navigation
This package is the producer entry: author paramSchema and WGSL, then inspect
the cooked receipt before the render owner admits a draw. The consumer route is
packages/render/README.md; host
assembly and recovery are documented in
packages/runtime/README.md.
An Alpha Mask cutoff, texture/sampler slot, or skin palette mismatch is a
structured producer failure, not a CPU duplicate-draw request.
Offline recovery is always inspect -> repair this producer -> retry:
shader source/schema -> cook + reflection receipt -> runtime catalog -> render prepareShader module catalog
ShaderRegistry owns the content-addressed build manifest. It is a runtime
lookup boundary, not an authoring store.
| Entry | Shape | Description |
|:--|:--|:--|
| ShaderRegistry.loadManifest() | () => Promise<Result<void, ShaderError>> | Load and validate the manifest |
| ShaderRegistry.forkForDevice(device) | (ShaderRegistryDevice) => ShaderRegistry | Fork validated CPU manifest inputs into an independent registry with an empty GPU cache; device-specific material variants are prepared on the candidate |
| ShaderRegistry.get(hash) | (string) => Result<ShaderModule, RhiError \| ShaderError> | Resolve an engine module by content hash |
| ShaderRegistry.entries() | () => IterableIterator<ManifestEntry> | Enumerate published content-addressed modules in manifest order |
| ShaderRegistry.materialShaderManifestEntries() | () => IterableIterator<MaterialShaderManifestEntry> | Enumerate validated material shader manifest rows |
| ShaderRegistry.findMaterialArtifact(id) | (string) => Result<MaterialArtifact, ShaderError> | Find the published module selected by a cooked material |
| ShaderRegistry.materialShaderIdentifiers() | () => IterableIterator<string> | Enumerate published material module identifiers |
loadManifest() validates the complete document before publication. A malformed
entry or material shader row returns one manifest-malformed error and leaves
entries(), materialShaderManifestEntries(), and lazy get() resolution
unchanged. Repair the same source and retry on the same registry; a successful
load publishes rows in source order and a later call is idempotent.
Device recovery uses forkForDevice to retain the admitted, validated CPU
manifest without another network request or JSON parse. The candidate has
independent registration maps and an empty module cache; candidate preparation
selects the material shader variant for the replacement device. After prepare,
the recovery transaction carries over user-installed artifacts and registrations
that were admitted while the candidate was being built. A shader-preparation
failure or timeout never reaches that carryover; later candidate failures discard
the candidate without publication, and it cannot return a lost-device handle.
An unloaded or failed source registry stays unloaded in its fork, so the original
validation and source-failure behavior still applies.
The WGSL-level module and the RHI GPU handle are different concepts. This
package owns the former; @forgeax/engine-rhi owns the latter.
Contract derivation
The compiler derives binding layout, uniform offsets, texture field names, and
the injection boundary from the material parameter contract. WGSL reflection
must agree with that derived shape before a record is published. Per-slot
texture coordinates are data in MaterialTextureValue, not a global shader
switch.
Standard texture availability comes from authored parameter declarations, not
GPU residency. Cooked materials omit unavailable texture declarations and
sampling code. Shared built-in Standard modules retain a fixed binding layout
and specialize sampling with one pipeline override, STANDARD_TEXTURE_MASK_OVERRIDE.
standardTextureMask() derives its bits from the canonical texture-field order;
the renderer carries the authored mask separately from the canonical UBO schema
and includes it in pipeline identity. Runtime selects published WGSL and GPU
pipeline constants; it does not compose or cook WGSL. A declared but nonresident
texture remains enabled and follows the existing resource recovery route.
The override's low 20 bits retain presence; the higher bits select an earlier
sample for each independent metallic, roughness, or alpha input when texture,
sampler, and coordinates match. The selector is zero when that input needs its
own sample. This changes sampling work without changing the Standard binding
layout or the authored texture values.
Bits 29 and 30 (STANDARD_TRIPLANAR_PROJECTION_BIT,
STANDARD_OBJECT_SPACE_NORMAL_BIT) are projection selectors that
standardProjectionMask() derives from the effective triplanarSpace and
normalMapSpace values. The WGSL reads them through
standardUsesTriplanarProjection() / standardUsesObjectSpaceNormal(), so
every texture sample stays in uniform control flow; a per-row value never
gates a sample. The ray entry is unspecialized, which is why ray admission
refuses those values.
Error recovery
Material and shader errors are closed. Switch on error.code, then use the
code-specific detail and hint:
| Code | Meaning | Recovery |
|:--|:--|:--|
| material-contract-program-mismatch | A pass does not satisfy the root contract | Align the pass module and re-cook |
| material-reflection-binding-mismatch | Reflection differs from the derived bindings | Fix WGSL or parameters, then re-cook |
| shader-module-not-found | The published module is absent | Add it to the build source catalog and rebuild |
| material-specialization-not-cooked | No runtime artifact exists for the selection | Run the cook path and publish its record |
| material-specialization-stale-generation | A dependency changed after cooking | Wait for dependencies to settle and re-cook |
| material-surface-slot-missing | Standard pass has no surface module slot | Add the slot to the authored pass and re-cook |
| material-surface-abi-mismatch | Surface export does not match the ABI required by the selected material model | Repair the producer-reported expected signature and re-cook; Standard uses surface_v1, while single-layer medium uses single_layer_medium_surface_v1 |
| material-surface-forbidden-interface | Surface declares a stage, resource, or vertex mutation | Remove the forbidden interface and re-cook |
| material-physical-contract-invalid | A root physical layer is incomplete or unsupported | Repair the root parameter fragment before composing or cooking |
Single-layer medium frame resources use the same declared shader ABI at 1x and
4x. Render resolves 4x opaque and nearest-water inputs by selecting the nearest
depth sample and the color from that exact sample. The composed medium shader
therefore reads two single-sample r32float depth facts; it does not read a
multisampled depth attachment or an averaged color resolve. Unsupported sample
counts fail at RenderGraph admission before this shader is recorded.
At an undisplaced refraction coordinate, the medium shader uses textureLoad
for the paired color texel. A neighborhood filter there would average across a
thin foreground or shoreline and undo the color/depth association. A displaced
rough-refraction footprint may average only after the complete color/depth
neighborhood passes the shared validity test. The executable 1x/4x linear-HDR
edge oracle and its strict direct/GPU receipt path are documented in the
Render single-layer medium contract.
Never hide one of these errors by creating an app-local artifact or changing a demo's material shape.
For the canonical error detail and executable recovery fields, use
material/errors.ts. The producer recovery
path is inspect -> repair authored source -> cold-cook -> verify publication;
runtime does not reinterpret a Surface error.
Temporal-v1 accessor
forgeax_scene_temporal is the single WGSL accessor ABI for the
forgeax::scene-data::temporal-v1 sampled data. Standard PBR producers pack
the shared SceneTemporalV1 record, and TAA resolve samples it through the
same accessors. Consumers must not add a private velocity or G-buffer unpack,
or import compiler, Naga, or backend policy into runtime shader code.
The public render route supplies the semantic target; this package supplies the
stable shader module identifier SCENE_DATA_TEMPORAL_V1_SHADER_MODULE. Layout,
clear values, invalid-depth handling, and reactive fallback remain owned by the
shared accessor source.
Portable Motion Blur
motion-blur.wgsl is the sole built-in raster Motion Blur producer. It samples
the shared forgeax_scene_temporal accessor, derives a target-rate-scaled and
clamped motion vector, probes a fixed eight-neighbour candidate set, and then
performs a depth-aware symmetric gather of at most 16 color taps while
preserving alpha. Invalid interval, reactive, reset, subpixel, and
depth-rejected samples return the current color; the shader never reads or
writes a private velocity buffer, G-buffer, storage texture, or TAA history.
Runtime code selects the pass through the typed render plan; shader lookup
remains a published content-addressed artifact and never imports compiler,
Naga, or backend policy.
Points and Lines shader contract
Points and Lines use the engine-owned forgeax::points-lines material shader
manifest row. The row is a content-addressed runtime artifact selected by the
same MaterialAsset and Materials.unlit route as other built-in materials.
The Standard renderer binds the Points/Lines view at group 0, binding 10, then
uses the prepared expansion geometry in the active main geometry pass.
The runtime contract is compiler-free: it loads a published artifact and never
imports Naga, a shader compiler, or WGSL authoring helpers. paramSchema
remains the single source for derived layout and uniform shape. Manifest
validation is atomic; a malformed or stale row leaves the prior published
catalog unchanged and returns a structured error.
| Evidence | Meaning |
|:--|:--|
| shader manifest row | forgeax::points-lines is published and content-addressed |
| runtime isolation triple | no Naga in dist, no runtime shader compiler dependency, and no compiler import |
| graph contract | points-lines uses the existing Standard main pass and material binding path |
| lane contract | direct WebGPU is the focused runtime route; clustered unlit and WebGL2 restrictions are structural contracts unless a lane-specific runtime probe is available |
When lookup or reflection fails, inspect .code, .expected, .hint, and the
code-specific .detail; repair the source or cook input and republish through
the build-time producer. Do not create an app-local WGSL module, bypass the
manifest, or add a backend-specific shader branch. The same retained/prepared
state supplies render inspection and recovery evidence.
Built-in PBR contract
The built-in PBR shader has named coordinate records for base color, metallic
roughness, normal, specular weight/color, emissive, occlusion, transmission,
and the declared physical texture slots. Each record carries offset, scale,
rotation, coordinate set, and physical extent metadata. The render package
projects the records into the UBO; the shader selects the declared vertex
coordinate input per slot. specular/specularColor replace the removed
The former specular-tint vocabulary was removed in one cut; no compatibility alias is supported.
Physical layer declarations are lowered from the Standard root contract. The compiler appends only authored physical resources after the engine-owned IBL and transmission region, so a base-only root has no physical binding or sample and retains its Deferred pass. The five glTF extensions use the same schema and the same Forward physical evaluator; runtime never compiles or patches this layout.
The diffuse-transmission layer (DIFFUSE_TRANSMISSION_AVAILABLE) adds a back-hemisphere
Lambert lobe to every Standard light evaluator and samples irradiance along -N. The factor
splits energy rather than adding it: the front diffuse albedo is scaled by 1 - factor before the
transmitted albedo factor · (1 - metallic) · tint is formed (render README §Diffuse transmission). The
irradiance cube has one mip, so sampleIblDiffuse uses an explicit level 0, which keeps
the lookup legal inside the non-uniform frontFacing-dependent transmission branch.
The IBL region has six bindings: irradiance texture/sampler, prefilter texture/sampler, BRDF lookup texture, and skylight uniform. BRDF lookup uses the irradiance linear-clamp sampler; the separate prefilter sampler permits per-draw reflection-probe selection. Re-cook authored Standard roots after a binding-contract change so runtime and shader layouts agree.
References
Surface authoring checklist
- [x] Import
forgeax_material::surface_v1and return the eight-fieldSurfaceData. - [x] Read generated material parameters through
forgeax_material::parameters. - [x] Keep vertex, light, BRDF, pass, binding, and output ownership in Engine modules.
- [x] Let the root parameter contract determine physical layers and pass admission.
- [ ] Add an engine entry point or resource binding to a Surface module.
The final unchecked item is intentionally forbidden. If an effect needs its own entry points or render targets, declare an explicit full-custom material and publish its pass/lane provenance instead of disguising it as a Standard Surface.
Metadata-only source catalogs
new ShaderRegistry({ manifestUrl }) loads manifest and material metadata without acquiring a
GPU device. GPU module lookup returns rhi-not-available until a device-backed
registry is used. Render Worker source asset loading uses this catalog; the
receiver installs immutable artifacts into its existing device-backed registry.
Standard deferred lighting contract
Standard and custom base Surface programs exchange SurfaceData with the
Engine's rigid or skinned material entry. standard-gbuffer.wgsl encodes that
result once; standard-deferred-lighting.wgsl resolves its lighting without
calling Surface or replaying receiver geometry. Alpha clipping happens before
any G-buffer output. Physical/medium Surface admission continues to select its
explicit Forward lane when the base deferred schema cannot represent its lobes.
The shared base BRDF/IBL callsites live in standard-lighting.wgsl. Its helpers
accept textures and material facts, so Forward and Deferred share light semantics
without sharing a material bind-group layout. The cooked utility manifest entry
is forgeax::engine-standard-deferred-lighting; it is an Engine utility, not a
runtime material publication. Clustered and unclustered sources are prewarmed
with the same device capability axes as Standard.
See the Render G-buffer contract for attachment formats, packing, resource limits and SSR outputs.
Shared clipping coverage
forgeax_clipping::planes owns clippedByPlanes, applyLocalClipping, and
applyViewClipping. World-space negative signed distance is discarded. View
coefficients append to the shared View ABI at byte 1024; its payload is 1136
bytes in a 1280-byte aligned slot. Color, shadow and temporal geometry must use
the same transformed world position. Material presence is derived from the root
parameter declaration during cook; runtime does not add feature defines.
Hashed alpha coverage
alpha-hash.wgsl owns the derivative-scaled object-space threshold shared by
Standard Surface, Unlit and temporal coverage. alphaHash is a default-zero
f32 material value (Materials projects its boolean option to 0/1). Its
Standard schema slot occupies the alignment gap before specularColor, so
existing subsequent uniform offsets remain unchanged. The shader producer
includes the helper in the source closure; runtime never cooks this source.
Sparse authored Standard contracts derive ALPHA_HASH_AVAILABLE from the
alphaHash declaration, so omitted fields produce disabled coverage without
adding hidden parameters.
See Alpha Hash authoring and verification.
Standard scalar map ABI
The default Surface evaluates independent metallic, roughness and alpha maps with per-slot UV data and R/G/B/A selectors. Its contract is documented in Render. The canonical schema generates scalar layout, coordinate records and texture-presence bits; Forward, Deferred, skinned and ShadowCaster share the Surface implementation.
Physical-map bindings start at STANDARD_PHYSICAL_BINDING_START, after the
reserved scene-material and global-prefilter slots. Compiler lowering and
renderer BGL/BG assembly consume that same constant, keeping expanded base-map
and IBL bindings disjoint. Re-cook materials after this ABI change.
Single-layer medium planar reflection
The medium template keeps the empty authored parameter prefix and renderer-owned IBL/backdrop/depth injections. Bindings 15 and 16 carry the linear 2D planar reflection and a 96-byte uniform (capture view-projection, normalized world plane, enabled flag). No capture uses the existing environment term. Valid matching plane samples replace only that reflection term, leaving Fresnel, foam and transmission integration unchanged. Producer and sampler run through Render's existing target/frame lifecycle; the shader owns no capture scheduling.
Standard displacement contract
standard-displacement.wgsl owns the normalized-height vertex formula and
triangle normal reconstruction. Standard rigid/skin, temporal and shadow
entries call the same level-zero sampling kernel. The existing Standard
parameter schema owns displacementTexture, displacementScale and
displacementBias; texture coordinates remain per-slot. Material
specialization constants apply to both vertex and fragment stages.
See Standard vertex displacement
for authoring, density and bounds behavior. Pack cooking validates both rigid
and skinned color/shadow programs in
shader-compiler/src/material/__tests__/standard-displacement.integration.test.ts.
Ray Surface reference context
The ordinary Engine manifest publishes forgeax_ray::query,
forgeax_ray::path_tracer and forgeax_ray::raster_source utility programs.
Query and transport share ray-traversal.wgsl, including their triangle/node
storage bindings. The build owns composition and reflection; runtime consumes
published WGSL and the material artifacts below. These programs alone do not
install a per-frame GI pass in Renderer.
The Standard SurfaceInput ABI includes uvFootprint0/1, eight conservative UV
diameters. Raster adapters supply zero and continue to use implicit derivatives;
the build-only ray context selects explicit mip levels from the hit's ray-cone
footprint. Custom Surface modules can consume the same input and shared sampling
helper. The reference admits opaque and MASK coverage; shared Standard opacity
<= alphaClipThreshold rejects a candidate only when the threshold is positive.
Surface status 3 means rejected coverage, while status 2 means invalid output.
geometricNormalWS describes
triangle orientation; vertexNormalWS is the interpolated normal used by Standard
and custom Surface shading. Authored normal maps use that vertex/tangent frame.
Raster adapters derive geometry from position derivatives; exact hits and cards
derive it from the indexed triangle, correcting mirrored winding.
RaySurfaceProgram carries build-produced WGSL, the derived parameter schema and
the resolved asset contract. admitRayMaterial(asset, material, context) shares
build/runtime admission, defaulting to ray-hit. card-capture additionally
requires the canonical Standard Surface; its restriction does not narrow ray
qualification. The ordinary material product publishes both eligible contexts;
runtime selects immutable artifacts without importing a compiler. The Standard direct-light BRDF and opaque ray BSDF share
one evaluator; sampling reports the full cosine/GGX-NDF mixture density and
explicit null events, including finite mapped/smoothed normals across the geometric
hemisphere. Surface admission validates values; BSDF value/PDF own directional
hemisphere clipping. Existing Standard energy conventions remain unchanged.
Direct material parameter regions
A producer receipt records the actual aligned parameter region. A direct program
with no parameter fields has materialRow: { byteLength: 0, fields: [] }; this
records the absence of a uniform region and does not allocate a placeholder row.
A scene-index program requires a positive aligned GPU Scene row. The shared ABI
validator rejects a zero-byte row with fields or a scene-index entry. Every cooked
raster selection carries its reflected direct entry, geometry inputs and ABI,
including custom programs combined with the Engine default shadow wrapper.
Surface lighting-channel transport
Standard rigid/skin and Deferred share common.wgsl::lightingChannelsMatch.
evaluateStandardDirect and evaluateStandardClusterLights accept the receiver
u32 mask explicitly. View.lightingChannels occupies the directional padding
word (byte 76). DirectLightSlot.channels.x is the surface mask in row 5 of the
96-byte local-light ABI; volume consumers preserve this stride and do not apply
surface policy. Mesh.surface is an integer tail: x/y carry visible-surface
row/count, z carries receiver channels. Scene-index variants read the same
GpuScenePrimitive.lightingChannels and transport it flat to fragments.
GBufferOutput.receiver_geometry is vec2<u32> (packed normal/family, mask).
Do not transport masks through UNORM encoding or numerical f32 conversion.
See the Render channel contract
for author validation, support and shadow/GI boundaries.
