Effect Format And Shader Runtime
FOnline uses .fofx files for authored GPU effects. One source combines render
state, one or more vertex/fragment shader passes, Engine-owned shader resources,
and the data needed to bake backend-specific shader artifacts.
Use this guide for authoring and runtime behavior. Use the generated effect-format reference, its focused render-state, resource, baking, and runtime pages, plus the canonical JSON model, for the exact current-revision contract.
Scope and authority
The owning sources are:
Source/Tools/EffectBaker.cppfor.fofxparsing, shader compilation, reflection, backend flavors, and diagnostics;Source/Frontend/Rendering.handSource/Frontend/Rendering.cppfor vertex layouts, built-in buffers, pass/render state, and reflected-resource loading;Source/Client/EffectManager.cppfor path caching, default effects,ScriptValueBuf, and per-frame buffer updates;Source/Client/Client.cppandSource/Scripting/ClientGlobalScriptMethods.cppfor script-selected effects and script-value APIs;Source/Frontend/Rendering-*.cppfor backend shader loading, pipelines, descriptor binding, and drawing;Source/Tests/Test_EffectBaker.cppfor executable bake and failure examples.
This page is reusable Engine documentation. An embedding project owns its
effect catalog, resource-pack override order, shader quality profile, art
direction, concrete EffectType assignments, and every semantic meaning
assigned to ScriptValueBuf.
BuildTools/EffectFormatInterface.json is the source-backed structured
contract. BuildTools/docs_effect_format.py validates its source anchors,
derives compile-limit defaults from the CMake project interface, and renders
the generated reference. Parser, baker, resource, or runtime drift must update
that model in the same change.
Minimal effect
A one-pass untextured quad effect can be as small as:
[Effect]
[VertexShader]
layout(set = 0, binding = 0, std140) uniform ProjBuf
{
mat4 ProjMatrix;
};
layout(location = 0) in vec3 InPosition;
void main(void)
{
gl_Position = ProjMatrix * vec4(InPosition, 1.0);
}
[FragmentShader]
layout(location = 0) out vec4 FragColor;
void main(void)
{
FragColor = vec4(1.0);
}
The file must have [Effect] and usable vertex/fragment source for every
declared pass. The baker supplies the version directive, precision qualifier,
and Engine compile-time defines.
File structure
.fofx uses the Engine ConfigFile parser in content-collection mode. Config
keys belong in [Effect]; shader section bodies are collected as raw text.
[Effect]
This required section owns:
Version;Passes;ShadowPassin 3D builds;BlendFunc;BlendEquation;DepthWrite;DepthFunc;DepthVariants;CullVariants;- the
_PassNvariants of blend and depth keys.
The runtime reparses this section from the baked copy of the original .fofx
file. Render state is therefore not encoded only in shader binaries or
reflection metadata.
[ShaderCommon]
This optional raw-text section is prepended to both stages of every pass. Use it for constants and helper functions shared by the effect’s stages/passes.
There is no .fofx include directive. Keep reusable code inside
[ShaderCommon], duplicate deliberately between separate effect resources, or
generate project effects outside the Engine format if the project owns such a
workflow.
Shader stages and pass fallback
For pass N, the baker resolves stages in this order:
[VertexShader PassN], then[VertexShader];[FragmentShader PassN], then[FragmentShader].
An empty pass-specific section is treated as absent and falls back to the generic section. If neither source exists for a stage, baking fails.
Pass numbering is one-based. Keep the spelling distinction clear:
- shader section:
[FragmentShader Pass2]; - state key:
BlendFunc_Pass2.
Render state
Pass count and shader version
Passes defaults to 1 and must be in
1..FO_EFFECT_MAX_PASSES (Engine default 6). The value controls every stage
compile, metadata artifact, and backend pass object.
Version defaults to 310. The baker emits:
#version 310 es
precision highp float;
Do not write a second #version directive in authored shader text.
Blend state
BlendFunc defaults to:
BlendFunc = SrcAlpha InvSrcAlpha
It must contain exactly two factors, source first:
Zero, One, SrcColor, InvSrcColor, DstColor, InvDstColor,
SrcAlpha, InvSrcAlpha, DstAlpha, InvDstAlpha, ConstantColor,
InvConstantColor, or SrcAlphaSaturate.
BlendEquation defaults to FuncAdd. Accepted values are FuncAdd,
FuncSubtract, FuncReverseSubtract, Max, and Min.
Use BlendFunc_PassN and BlendEquation_PassN for pass-local overrides.
Unknown values fail effect construction.
Depth state
DepthWrite defaults to True. DepthFunc defaults to Always; accepted
comparisons are Always, Never, Less, LessEqual, Equal,
GreaterEqual, Greater, and NotEqual.
Use DepthWrite_PassN and DepthFunc_PassN for pass-local overrides.
Depth state is active for EffectUsage::QuadSprite and, in 3D builds,
EffectUsage::Model when the target has a depth attachment. UI, primitives,
lights, and final blits may draw to targets where depth state has no effect.
DepthVariants defaults to False. Enable it only when a runtime caller must
select depth test/write behavior per draw, as Effekseer does for emitter nodes.
An opted-in effect builds four states: test/write, test/no-write,
no-test/write, and no-test/no-write. The test variants retain the authored
DepthFunc; no-test uses Always. Without the opt-in, a draw may use only the
state resolved from the authored DepthWrite and DepthFunc values.
See Frontend and Rendering for map depth ordering and backend-specific rendering behavior.
Per-draw culling
CullVariants defaults to False. The default accepts only
CullModeType::None. Enable it when model or particle callers need to choose
None, Back, or Front face culling per draw. Backends that bake culling
into device state or pipelines then build the additional variants; a request
for a variant the effect did not opt into fails instead of silently drawing
with the wrong state.
Shadow pass
In a 3D build, ShadowPass defaults to -1. Set it to a one-based pass index
to mark that pass as the model shadow pass. The index is validated against the
compiled pass limit. Runtime model drawing can disable marked shadow passes
without disabling the other passes.
Shader compiler environment
Each pass is parsed and linked through glslang as GLSL for Vulkan 1.0 and
SPIR-V 1.0. Before [ShaderCommon] and the stage body, the baker supplies:
#version <Version> es
precision highp float;
#define MAX_SCRIPT_VALUES <FO_EFFECT_SCRIPT_VALUES>
When FO_ENABLE_3D is active, it also supplies:
#define MAX_BONES <FO_MODEL_MAX_BONES>
#define MAX_TEXTURES <FO_MODEL_MAX_TEXTURES>
The linked program must build reflection successfully. Vertex outputs and fragment inputs therefore need compatible locations/types even if one backend would otherwise accept looser source.
Vertex input contract
Effect usage is fixed when a path is first loaded. The shader’s input locations must match that usage.
ImGui, QuadSprite, and Primitive
These usages share Vertex2D:
| Location | GLSL type | Native field | Meaning |
|---|---|---|---|
0 |
vec3 |
PosX, PosY, PosZ |
position/depth |
1 |
vec4 |
Color |
normalized vertex color |
2 |
vec2 |
TexU, TexV |
texture coordinate |
3 |
vec2 |
EggFlags |
egg or draw-path auxiliary data |
A shader may omit unused inputs. Keep declared locations/types compatible with the table.
Model
EffectUsage::Model uses Vertex3D:
| Location | GLSL type | Meaning |
|---|---|---|
0 |
vec3 |
position |
1 |
vec3 |
normal |
2 |
vec2 |
primary texture coordinate |
3 |
vec2 |
base/secondary texture coordinate |
4 |
vec3 |
tangent |
5 |
vec3 |
bitangent |
6 |
vec4 |
blend weights |
7 |
vec4 |
blend indices |
8 |
vec4 |
normalized vertex color |
Model effects exist only in 3D builds. Keep
FO_MODEL_BONES_PER_VERTEX = 4; active backend layouts assert that shape.
Descriptor and binding contract
Native convention
Author resources for the native Vulkan convention:
- descriptor set
0: uniform buffers; - descriptor set
1: combined image samplers.
Bindings are explicit integers. Bindings need not be dense for the native path, but they must be unique within one shader stage and resource class.
The EffectBaker tests contain sources that omit set = ...; glslang can still
compile those fixtures. Production effects should state sets explicitly. The
native Vulkan backend consumes the original SPIR-V and does not repair an
incorrect authored descriptor set.
SDL_GPU remap
The baker makes a copy of native SPIR-V and rewrites descriptor decorations to:
| Stage/resource | Descriptor set |
|---|---|
| vertex samplers | 0 |
| vertex uniform buffers | 1 |
| fragment samplers | 2 |
| fragment uniform buffers | 3 |
Within each class/stage, authored bindings are sorted and rewritten to dense
slots 0..N-1. Each stage is limited to 16 samplers and 4 uniform buffers.
The remapped module is emitted as spv_sdl; the SDL Metal source is also
compiled from this remapped module.
Missing bindings, duplicate same-stage bindings, storage images, and dead descriptor declarations fail the bake because the remap cannot be made complete and deterministic.
Engine-provided textures
The runtime recognizes these sampler names:
| Sampler | Availability | Producer |
|---|---|---|
MainTex |
all builds | current sprite/render-target/model primary texture |
IndoorMaskTex |
client map paths | current indoor mask |
BackgroundTex |
caller-provided direct-scene paths | snapshot of the scene before a refracting draw |
ModelTex0..ModelTexN-1 |
3D builds | model texture slots |
Unknown sampler names are reflected, but RenderEffect has no Engine producer
for them. A source can therefore compile while the runtime leaves the sampler
unbound. Treat the table as the authoring allowlist unless a renderer/runtime
change adds a producer in the same change.
Built-in uniform buffers
Uniform block names and byte layouts are fixed. EffectBaker compares reflected sizes to the native structures and rejects unknown blocks.
General buffers
| Block | GLSL shape | Producer/meaning |
|---|---|---|
ProjBuf |
mat4 ProjMatrix |
current 2D or 3D projection |
MainTexBuf |
vec4 MainTexSize |
width, height, reciprocal width/height |
EggBuf |
vec4 EggData[3] |
two egg masks plus transition parameter |
SpriteBorderBuf |
vec4 SpriteBorder |
sprite atlas UV rectangle |
ParticleSamplingBuf |
vec4 ParticleSampling |
per-draw particle sampling, atlas-clamp, distortion, and background-orientation controls |
TimeBuf |
vec4 FrameTime; vec4 GameTime |
.x seconds, session-relative, wrapped at 8192 |
RandomValueBuf |
vec4 RandomValue |
four per-frame random values in [0,1] |
ScriptValueBuf |
vec4 ScriptValue[MAX_SCRIPT_VALUES / 4] |
project-controlled float slots |
CameraBuf |
vec4 MapAnchorScreenPos; vec4 ChunkScreenAnchor |
world/screen affine UV bases |
For CameraBuf, evaluate either basis as:
vec2 uv = Basis.xy + TexCoord * Basis.zw;
MapAnchorScreenPos is world anchored and zoom invariant.
ChunkScreenAnchor is screen anchored. Do not reduce either to a subtraction;
the scale terms account for padded/chunked render targets.
ParticleSamplingBuf is populated by particle runtimes rather than by the
general sprite path. Its component meanings belong to the selected particle
effect/runtime pair; the stock Effekseer effects use them for point sampling,
atlas-safe clamping, distortion intensity, and vertical orientation of
BackgroundTex.
Model buffers
| Block | GLSL shape | Meaning |
|---|---|---|
ModelBuf |
vec4 LightColor; vec4 GroundPosition; mat4 WorldMatrices[MAX_BONES] |
lighting, ground anchor, skin matrices |
ModelTexBuf |
vec4 TexAtlasOffset[MAX_TEXTURES]; vec4 TexSize[MAX_TEXTURES] |
atlas transforms and texture dimensions |
ModelAnimBuf |
vec4 AnimNormalizedTime; vec4 AnimAbsoluteTime |
normalized and looped absolute animation time |
Storage buffers and storage images are unsupported.
ScriptValueBuf ownership and lifetime
FO_EFFECT_SCRIPT_VALUES defaults to 16; an embedding project may override
it. The value must be positive and divisible by four. It is a compiled Engine
shape and must match the generated/baked shader define.
The runtime behavior is:
- The first load of an effect that declares
ScriptValueBufcreates a zeroed buffer. - Script writes mutate the cached
RenderEffectobject. - Values remain until overwritten or explicitly cleared.
Game.SetEffect(...)changes the selected object but does not clear either object’s values.- Returning to a previously loaded path returns its previous buffer contents.
Game.ClearEffectScriptValues(...)zeroes the selected object’s whole buffer.
The cache key is the resource path only. If the same path is selected in
multiple compatible slots, they share one buffer. Projects should assign each
slot range one owner and document collisions. Re-pushing values after a
variant swap is still a good project practice when variants use different
paths or buffer declarations, but it is not a reset guarantee from
SetEffect. SetEffect does not reset cached values.
Runtime loading and cache identity
EffectManager::LoadEffect(usage, path) returns an existing cached object when
the path was loaded before. The cache key does not include EffectUsage.
Therefore the first load fixes the object’s usage and backend pipeline/input
layout assumptions.
Do not reuse one path across incompatible categories:
ImGui;QuadSprite;Primitive;Model.
Separate resources may contain identical shader text when they need different usages. Path clarity is more important than avoiding a tiny source duplicate.
The runtime loads:
- the baked
.fofxsource for pass count and render state; .fofx-N-infofor reflected native and SDL resource slots;- the backend flavor required by the active renderer.
Missing source, metadata, or shader flavor is a load error.
Script API
The client/mapper script surface is:
Game.SetEffect(effectType, effectSubtype, effectPath);
Game.SetEffectScriptValue(effectType, effectSubtype, valueIndex, value);
Game.SetEffectScriptValues(
effectType,
effectSubtype,
valueStartIndex,
values,
valuesOffset = 0,
valuesCount = -1);
Game.ClearEffectScriptValues(effectType, effectSubtype);
SetEffect uses an empty path to restore the slot’s default. A non-empty path
loads using the default slot’s usage.
Script-value calls resolve the currently selected target and fail when:
- the type/subtype is unsupported or invalid;
- the target entity/offscreen slot does not exist;
- the effect is not loaded;
- the effect does not declare
ScriptValueBuf; - an input or destination range is out of bounds.
Use the ranged method for parameter blocks updated together. It validates
valuesOffset, derives the remaining count when valuesCount = -1, and makes
one native write.
Per-font ScriptValue writes are not supported; EffectType::Font accepts only
subtype -1 for the shared font effect. GenericSprite and CritterSprite
can target the shared slot with subtype 0 or a live entity by id. Offscreen
subtypes must have been registered and loaded.
Baking outputs
For each pass and stage, EffectBaker emits:
| Flavor | Consumer |
|---|---|
spv |
native Vulkan; source for GLSL/ES/HLSL cross-compilation |
spv_sdl |
SDL_GPU Vulkan path |
glsl |
OpenGL desktop (330) |
glsl_es |
OpenGL ES/WebGL (300 es) |
hlsl |
Intermediate HLSL Shader Model 4.0, compiled at bake time |
dxbc |
Direct3D 11 bytecode, compiled from HLSL by vendored vkd3d-shader |
msl_mac |
SDL_GPU Metal on macOS |
msl_ios |
SDL_GPU Metal on iOS |
With Baking.Direct3DLevel9Shaders = True, the baker also embeds an Aon9
level-9.3 bytecode chunk in non-model effects. A shader that exceeds that
profile fails baking; 3D-model effects do not receive it. The default is off.
This is an opt-in for 2D-only Direct3D builds, not support for 9.1/9.2.
Naming is:
<path>.fofx-<pass>-<vert|frag>-<flavor>
<path>.fofx-<pass>-info
[EffectInfo] stores program-wide reflected bindings and proves built-in
uniform-buffer sizes. [EffectInfoSdl] stores stage-local dense slots and
sampler/UBO counts. The original source is copied to the baked resource path.
Resource packs and overrides
The Engine provides minimal-profile effects in Resources/Core/Effects/ and
bootstrap effects in Resources/Embedded/Effects/. An embedding project may
provide a later resource with the same path to shadow an Engine default.
Keep reusable Engine defaults conservative. Richer project copies may target a higher hardware profile, but the project must validate every shipped backend and preserve a deliberate fallback. See Frontend and Rendering for the current default slot map and minimal-profile policy.
Authoring practices
- Start from the closest Engine effect with the same
EffectUsage, vertex inputs, and built-in buffers. - Declare descriptor sets and bindings explicitly, even when a test fixture demonstrates that glslang can infer a set.
- Keep pass count small. Every pass multiplies stage compilation, metadata, backend objects, and draw work.
- Remove unused samplers and uniform blocks. Dead descriptor declarations are rejected for SDL remapping.
- Use only recognized buffer names and copy their layouts exactly.
- Keep
ScriptValueBufslots centralized in project code/docs, with one owner per range and stable meanings across shader variants. - Prefer
SetEffectScriptValuesfor contiguous snapshots and explicitClearEffectScriptValueswhen reset semantics matter. - Do not use the same path for incompatible effect usages.
- Keep animation time periodic.
TimeBufwraps at8192seconds, and script-maintained float clocks should wrap before precision degrades. - Test depth and blending on a target that actually has the relevant attachments and draw order.
- Validate the lowest hardware/profile the project claims to support; a successful cross-compile is not a visual or driver guarantee.
Failure guide
| Symptom | Likely boundary |
|---|---|
| missing Effect/vertex/fragment error | section spelling, Passes, or fallback source |
| shader compiler diagnostic | GLSL syntax, version/profile, or stage interface |
| invalid uniform buffer size | block field order/type/array length differs from RenderEffect |
| invalid uniform buffer | unknown block name |
| explicit/duplicate binding error | missing or colliding stage-local binding |
| unused resource error | declared sampler/UBO is optimized out or never read |
| SDL stage-limit error | more than 16 samplers or 4 UBOs in one stage |
| effect loads but texture is empty | sampler name has no Engine producer or wrong set/binding |
| script-value write throws | wrong target, unloaded effect, no ScriptValueBuf, or bad range |
| script value appears in another slot | both slots resolve to the same cached path |
| switching back restores old tuning | expected path-cache persistence; clear or overwrite explicitly |
| works on one renderer only | backend flavor/profile/descriptor/depth difference |
Validation workflow
After changing an Engine effect-format source, buffer, backend, or built-in effect:
python BuildTools\docs_effect_format.py --write
python BuildTools\tests\test_docs_effect_format.py
python BuildTools\docs_effect_format.py --check
python BuildTools\docs_contract_diff.py --help
Run the focused native baker tests through the configured embedding-project
unit-test target. Source/Tests/Test_EffectBaker.cpp is the owning test file.
Renderer state changes also need the relevant backend and visible scene.
For an embedding project:
- regenerate/configure when a compile limit changed;
- bake resources;
- run project validators for effect paths,
EffectTypeassignments, and ScriptValue ownership; - launch representative visible scenes for every affected slot;
- test every shipped renderer/backend and minimum hardware profile;
- compare default/fallback and overridden project effects.
Change checklist
When the Engine contract changes, update together:
BuildTools/EffectFormatInterface.json;Docs/en/how-to/content/effect-format.md;- generated
Docs/generated/effect-format.jsonand reference pages; BuildTools/tests/test_docs_effect_format.py;Source/Tests/Test_EffectBaker.cppor the owning renderer/runtime test;- Baking Pipeline when output/compiler behavior changes;
- Frontend and Rendering when runtime/backend behavior changes;
- GeneratedApiAndMetadata.md and Generated Contract Change Management when the structured contract or aggregate diff surface changes;
- embedding-project docs/tests for paths, slot semantics, fallbacks, and visual validation.