The XNA stock effects: exact semantics, worked uses and verification history
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Defaults and exceptions are pinned by unit tests; renderer pixel results are CNA's own records for the fixtures that ran them; the defect history is taken from CNA's code comments and plans and not re-verified; nothing was executed for this page.
This page gives the exact behaviour of the five XNA stock effects that draw geometry — BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect and SkinnedEffect — as CNA implements them at this snapshot: defaults, formulas, ordering traps, what each property becomes in the draw packet, a worked use of each, the per-vertex versus per-pixel lighting choice, and the verification history that explains why the formulas look the way they do. It is for porters whose XNA scenes must look the same, and for maintainers touching the stock shaders. The overview and property tables are on Effects System; the object model underneath (parameters, passes, the GpuDrawParams packet) is on Effect object model.
A semantic reimplementation, checked against XNA
On most renderers the stock effects are not XNA's compiled programs. Each effect class translates its properties into GpuDrawParams, and each renderer selects its own CNA-authored shader variant for the combination (fog, vertex colour, lighting, texture, skinning). The formulas are copied from FNA's and XNA's shader sources and effect helpers — the code cites EffectHelpers.SetFogVector, EffectHelpers.SetMaterialColor and ComputeFresnelFactor by name — but correctness is a property of each renderer's shaders, not of the class. Two renderers execute XNA's own programs instead: DIRECTX9 recompiles Microsoft's stock .fx sources and FNA3D runs the FNA-shipped compiled binaries; both routes are compared on Four shader routes. The five geometry effects also load from XNB through built-in stock-effect readers (BasicEffectReader and its siblings), which need no compiled-effect capability. Which renderer draws which effect is tabulated on Effects System: renderer coverage.
BasicEffect
Surface and defaults
BasicEffect (BasicEffect.hpp) implements IEffectMatrices, IEffectFog, IEffectLights and the CNAEXT IShadowReceiverEXT. World, View, Projection and VertexColorEnabled are public data members and have get/set accessor pairs; XNA 4.0 has only properties (its IL shows private fields behind World/View/Projection properties), so the member spelling is a CNA convenience, not XNA parity. DirectionalLight0–2 are public DirectionalLight members as well as getDirectionalLightNProperty() results.
Defaults, each pinned by a test in BasicEffectTests.cpp: identity matrices; DiffuseColor one, EmissiveColor and AmbientLightColor zero, Alpha one, SpecularColor one, SpecularPower 16; LightingEnabled, PreferPerPixelLighting, FogEnabled, TextureEnabled and VertexColorEnabled false; FogStart 0, FogEnd 1, FogColor black; Texture null. DirectionalLight0 is enabled and lights 1 and 2 are disabled; every light starts with diffuse one, specular zero and direction Vector3::Down. Three of these were wrong in early CNA code (the VertexColorEnabled default, DirectionalLight0.Enabled and the light direction default); the tests now pin the XNA values.
What reaches the draw
- Material colour. With lighting on, the forwarded diffuse is
DiffuseColor × AlphaandEmissiveColor × Alphais added on the lit path. With lighting off no light term runs at all, so the emissive colour is folded into the forwarded diffuse,(DiffuseColor + EmissiveColor) × Alpha— FNA'sSetMaterialColor. Without this fold the emissive colour is silently lost on unlit draws, which is exactly what early CNA code did. - Disabled lights. FNA's
DirectionalLight.Enabledsetter zeroes the light's GPU-facing colours. CNA's setter has no side effect, soFillGpuDrawParamssends zero diffuse and specular for a disabled light. A shared early defect never checkedDirectionalLight0.Enabled, so a disabled key light kept lighting the surface. - Specular. Only on the lit path; the material
SpecularColormultiplies the summed per-light specular once, each light's own specular colour enters the sum. The eye position is the translation of the inverse view matrix. - Fog. A view-space fog vector baked from the third column of
World × View, divided byFogStart − FogEnd; zero when fog is off and{0,0,0,1}(fully fogged) whenFogStart == FogEnd. - Shader index.
OnApply()computes XNA'sShaderIndex: +1 without fog, +2 with vertex colour, +4 with texture, and for lighting +24 per-pixel, +16 when lights 1 and 2 are both disabled (XNA's one-light variant), otherwise +8. The DIRECTX9 and FNA3D renderers select real XNA programs from this index; the other renderers use the individual flags. - Vertex colour and texture combine. The textured, vertex-coloured and diffuse terms multiply. Early EasyGL code dropped
DiffuseColorin exactly that combination, and both EasyGL and Vulkan once ignored theVertexColorEnabledtoggle on untextured draws.
EnableDefaultLighting() overwrites — call it first
EnableDefaultLighting() sets LightingEnabled, the ambient colour (0.05333332, 0.09882354, 0.1819608), and unconditionally overwrites all three lights' diffuse colour, direction, specular colour and enabled flag with XNA's key/fill/back rig (key light direction (-0.5265408, -0.5735765, -0.6275069)). Tests check the exact constants. Customising a light and then calling it silently restores the defaults; the right order is rig first, adjustments second. This is XNA's own behaviour faithfully ported, not a CNA quirk.
Worked use: a lit, textured cube
BasicEffect effect(getGraphicsDeviceProperty());
effect.World = Matrix::CreateRotationY(rotationAngle); // or setWorldProperty(...)
effect.setViewProperty(view);
effect.setProjectionProperty(projection);
effect.setTextureEnabledProperty(true);
effect.setTextureProperty(&cubeTexture); // Texture2D*, not owned
effect.EnableDefaultLighting(); // rig first ...
effect.DirectionalLight0.setDiffuseColorProperty(Vector3(1.0f, 0.95f, 0.9f)); // ... then adjust
for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty())
{
pass.Apply();
getGraphicsDeviceProperty().DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, vertexCount, 0, indexCount / 3);
}
Read-checked against the TARGET headers (BasicEffect, DirectionalLight, Matrix::CreateRotationY, Game::getGraphicsDeviceProperty); not compiled. The texture pointer is not retained; keep cubeTexture alive, or hand ownership to the effect with the CNAEXT SetOwnedTexture(std::shared_ptr<Texture2D>).
AlphaTestEffect
Implements IEffectMatrices and IEffectFog only — no lighting. AlphaFunction is a CompareFunction defaulting to Greater; ReferenceAlpha is an int stored unclamped (default 0); VertexColorEnabled, DiffuseColor, Alpha and Texture complete the surface. It uses FNA-style dirty flags (AlphaTestEffect.cpp).
The comparison becomes one alphaTest vector that every renderer able to draw the effect evaluates the same way (PORTABLEGL has no discarding shader and refuses it by name). The reference is normalised as ReferenceAlpha / 255 and each ordering comparison is shifted by half a step, 0.5 / 255, so that Greater at 127 keeps alpha 128–255 exactly as XNA does; Equal and NotEqual test |a − ref| < 0.5/255; Always and Never ignore the reference. AlphaTestEffect has no TextureEnabled switch: every XNA shader variant samples its texture, so CNA always reports texturing on and renderers supply Direct3D 9's opaque-black sample for a null texture rather than silently selecting an untextured program. The alpha that is tested is the combined texture × diffuse × (optional) vertex-colour alpha. The effect performs a test, not blending: a pixel is kept whole or discarded.
Worked use: a cut-out chain-link fence
AlphaTestEffect effect(getGraphicsDeviceProperty());
effect.setWorldProperty(fenceWorld);
effect.setViewProperty(view);
effect.setProjectionProperty(projection);
effect.setTextureProperty(&fenceTexture); // alpha 255 = wire, 0 = gap
effect.setAlphaFunctionProperty(CompareFunction::Greater);
effect.setReferenceAlphaProperty(128); // keep alpha above 128
for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty())
{
pass.Apply();
getGraphicsDeviceProperty().DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, vertexCount, 0, indexCount / 3);
}
With a VertexPositionColorTexture fence the per-vertex colour multiplies only when VertexColorEnabled is true, and the product's alpha takes part in the discard. That case was once wrong on Vulkan, whose alpha-test pipeline declared no colour attribute; the fix added a vertex-coloured shader sibling and discriminating pixel tests. Read-checked; not compiled.
DualTextureEffect
Two independent texture slots (Texture, Texture2), VertexColorEnabled, DiffuseColor/Alpha and fog; no lighting. It reads two UV sets — the first texture from texture-coordinate usage index 0, the second from index 1 — so give the mesh both sets. XNA refuses a draw whose declaration lacks TextureCoordinate1; CNA has no such check (CNA-GAP-033), and what the second texture then sees depends on the renderer: (0, 0) on EasyGL, OPENGL4 and the others, but the first set's coordinates on VULKAN, which aliases TextureCoordinate1 onto TextureCoordinate0 (VULKAN-150). The shaders double the first texture's RGB before multiplying by the second (base.rgb *= 2.0 in the shared GL shader corpus GlStockShaderSources.hpp; the same factor in Vulkan's dual_texture3d fragment shader). An unbound texture samples opaque black.
The doubling is why a mid-grey (0.5) lightmap reproduces the base texture unchanged — the neutral value most lightmap bakers emit. It is also a lesson in test design: all three renderers of CNA's first effect-comparison campaign initially omitted the factor, and no earlier test noticed because fixtures built from saturated 0-or-1 colours cannot distinguish "multiplied by one" from "multiplied by two and clamped". A lightmap authored for XNA rendered visibly too dark until the factor was added.
Worked use: a lightmapped floor
DualTextureEffect effect(getGraphicsDeviceProperty());
effect.setWorldProperty(floorWorld);
effect.setViewProperty(view);
effect.setProjectionProperty(projection);
effect.setTextureProperty(&floorDiffuse); // UV set 0, tiled base colour
effect.setTexture2Property(&floorLightmap); // UV set 1, baked lighting; 0.5 = unchanged
for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty())
{
pass.Apply();
getGraphicsDeviceProperty().DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, vertexCount, 0, indexCount / 3);
}
The vertex type must carry both texture-coordinate sets; declare it with an explicit VertexDeclaration (see Vertex declarations and streams and Tutorial 55). Read-checked; not compiled.
EnvironmentMapEffect
Implements IEffectMatrices, IEffectLights and IEffectFog; takes a diffuse Texture, a TextureCube* EnvironmentMap, EnvironmentMapAmount (default 1), EnvironmentMapSpecular (default black), FresnelFactor (default 1), EmissiveColor and the three lights. As in XNA, LightingEnabled is always true and setting it to false throws NotSupportedException (SetLightingEnabledFalseThrows).
- Blend. The environment colour is interpolated with the lit base colour,
lerp(color.rgb, envmap.rgb, weight), whereenvmapis the cube sample already scaled by the combined alpha (texture alpha timesAlpha, as in XNA) and the weight is the Fresnel factor timesEnvironmentMapAmount. Early CNA code blended additively, which over-brightened. - Fresnel.
FresnelFactoris an exponent; 0 disables the Fresnel term and the weight becomesEnvironmentMapAmount. The weightpow(1 − |dot(eye, normal)|, FresnelFactor) × EnvironmentMapAmountis evaluated per vertex and clamped to 0–1 before interpolation, because XNA computes it in the vertex shader and Direct3D 9 saturates theCOLOR1register that carries it. Both the shared GL corpus and Vulkan'senv_map3dvertex shader do this; clamping per fragment instead gives a different gradient, and skipping the clamp letEnvironmentMapAmountabove 1 extrapolate the rim past the cube colour. (An earlier CNA revision evaluated the term per pixel; the current shaders document why that is not equivalent.) - Specular. Not a Phong highlight:
EnvironmentMapSpecularmultiplied by the cube map's alpha and by that same combined alpha, added after the blend. A non-black specular colour also sets the effect'sspecularEnabledflag (XNA's shader-variant switch). The flag travels in the draw packet and is read by DIRECTX9 (D3D9EffectDraw.cpp) and FNA3D (Fna3dDraw.cpp) to pick XNA's specular shader variant; the other renderers derive the specular term from the colour. The header comment saying no renderer reads it is stale (CNA-BUG-261); see the draw packet. - Normals. Transformed by the inverse transpose of the world matrix, which keeps reflections correct under non-uniform scale; EasyGL once used the world matrix itself.
- Three lights.
FillGpuDrawParamsforwards the direction and diffuse colour of all three lights, zeroed when a light is disabled; the renderers consume them. Early code forwarded only light 0, and a planning checkbox for that task stayed open after the implementation had moved on. When lights 1 and 2 are disabled the shader index selects XNA's one-light variant.
Worked use: a chrome car body lit by three lights
EnvironmentMapEffect effect(getGraphicsDeviceProperty());
effect.setWorldProperty(carBodyWorld);
effect.setViewProperty(view);
effect.setProjectionProperty(projection);
effect.setTextureProperty(&carBodyPaint);
effect.setEnvironmentMapProperty(&skybox); // TextureCube*
effect.setEnvironmentMapAmountProperty(0.85f); // mostly reflective
effect.setFresnelFactorProperty(1.0f); // brighter toward grazing angles
effect.DirectionalLight0.setEnabledProperty(true); // warm key
effect.DirectionalLight0.setDiffuseColorProperty(Vector3(1.0f, 0.85f, 0.7f));
effect.DirectionalLight1.setEnabledProperty(true); // cool fill
effect.DirectionalLight1.setDiffuseColorProperty(Vector3(0.2f, 0.3f, 0.5f));
effect.DirectionalLight2.setEnabledProperty(true); // rim from behind
effect.DirectionalLight2.setDiffuseColorProperty(Vector3(0.6f, 0.6f, 0.6f));
for (EffectPass& pass : effect.getCurrentTechniqueProperty()->getPassesProperty())
{
pass.Apply();
getGraphicsDeviceProperty().DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, vertexCount, 0, indexCount / 3);
}
A three-light scene is what distinguishes the current forwarding from the former one-light result; a single-light scene cannot. Calling setLightingEnabledProperty(true) is legal but unnecessary; false throws. Read-checked; not compiled.
SkinnedEffect
SkinnedEffect::MaxBones is 72. setWeightsPerVertexProperty accepts 1, 2 or 4 and throws ArgumentOutOfRangeException otherwise. SetBoneTransforms(const std::vector<Matrix>&) throws ArgumentNullException for an empty vector and ArgumentException for more than 72 matrices; GetBoneTransforms(count) throws for count outside 1–72 and restores M44 = 1 as FNA does (bones may be stored in 4×3 form). The constructor fills the palette with 72 identity matrices. Lighting is always on (setting it off throws), with specular (a Blinn–Phong half-vector term), fog and three directional lights. CNA adds a CNAEXT VertexColorEnabled member for imported glTF meshes with a COLOR_0 attribute; XNA's SkinnedEffect has no such property.
Only the first WeightsPerVertex weight/index pairs are summed, as in FNA's Skin(vin, boneCount) (SkinnedEffect.cpp). CNA's shaders once summed all four regardless, so a project that requested two weights but relied on that behaviour renders differently now: set WeightsPerVertex to what the asset really carries. Earlier revisions also dropped the specular colour and power on Clone(), forwarded only light 0 and had no GPU specular term; all three are fixed.
Worked use: driving the palette from AnimationPlayer
SkinnedEffect effect(getGraphicsDeviceProperty());
effect.setViewProperty(view);
effect.setProjectionProperty(projection);
effect.setWeightsPerVertexProperty(2); // must match the asset: 1, 2 or 4
effect.setSpecularColorProperty(Vector3(0.3f, 0.3f, 0.3f));
effect.setSpecularPowerProperty(16.0f);
effect.EnableDefaultLighting();
// every frame, after player.Update(elapsed, true):
effect.SetBoneTransforms(player.GetSkinTransforms()); // already multiplied by inverse bind poses
The CNAEXT AnimationPlayer::GetSkinTransforms() returns a const std::vector<Matrix>& that already contains each bone's inverse bind pose, the form the skinning shader expects. A rig with more than 72 palette joints must be split into draws with disjoint palettes. Read-checked; not compiled.
Per-vertex or per-pixel lighting
PreferPerPixelLighting (on BasicEffect and SkinnedEffect) defaults to false, XNA's per-vertex, Gouraud-interpolated default. An earlier shared dispatch forwarded neither this flag nor EnvironmentMapEffect's specular switch, so GPU renderers lit every fragment regardless of the setting; lit-scene baselines changed when the XNA default took effect (low-poly surfaces look more faceted and highlights soften under vertex interpolation). At this snapshot a lighting-path switch that reads GpuDrawParams::preferPerPixelLighting exists in the DIRECTX9, DIRECTX11, DIRECTX12, EasyGL, OPENGL4, VULKAN, WEBGPU (including its skinned family), SDL_GPU, METAL and SOFTWARE renderers, and FNA3D passes it to the selection of FNA's own compiled variants. Two qualifications:
- A draw that receives a shadow takes the per-pixel path whatever the setting says on VULKAN, WEBGPU, SDL_GPU and EasyGL: a shadow lookup evaluated at the vertices and interpolated would give a gradient, not a shadow.
AlphaTestEffectandDualTextureEffecthave no lighting, andEnvironmentMapEffecthas noPreferPerPixelLightingproperty in XNA either.
The switches were located by reading each renderer's source; whether each variant matches XNA pixel for pixel is established only where a fixture covers it (DIRECTX9 through the XNA oracle corpus; the other renderers through their own goldens).
The pattern behind the historical defects
The stock-effect audits that produced the fixes above found three recurring failure shapes, and they remain the first things to suspect when one renderer disagrees:
- A missing shader term — the doubled dual-texture RGB, the Fresnel weighting and its clamp, the specular term.
- A public flag that never reaches GPU state — the former
VertexColorEnabled,WeightsPerVertexandPreferPerPixelLightinggaps. - A shared dispatch defect affecting several renderers at once — the ignored
DirectionalLight0.Enabled, fog that was once a no-op on every Vulkan 3D shader, and the clone-state omissions.
The core interpolation and lighting formulas were not broadly wrong; the failures were specific omissions, and each closed one came with a discriminating fixture (a 0.5 lightmap rather than saturated colours, a three-light rather than one-light scene, a non-uniform Z scale of 20 rather than 4× so the reflection lands deep inside a cube face). These are historical cases: the current code paths are the ones described in the sections above. A byte-identical result on one renderer pair for one composed fixture does not generalise to untested effect combinations or renderer families.
Evidence and limits
The behaviour above was checked by reading the effect classes, the shared GL shader corpus and the Vulkan shaders at snapshot 009d40f5; nothing was built or executed. Defaults and exceptions are pinned by the unit tests named on this page (BasicEffectTests.cpp, EnvironmentMapEffectTests.cpp, SkinnedEffectTests.cpp, StockEffectCloneConstructorTests.cpp, StockEffectNullTextureTests.cpp). Renderer-level pixel results are CNA's own records for the renderers and fixtures that ran them; the defect history is recorded in CNA's code comments and plans, and is not re-verified here. XNA comparisons come from the genuine XNA 4.0 assembly's IL and the XNA effect sources vendored for DIRECTX9.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Effects System · Tutorial 32: BasicEffect · Tutorial 54: AlphaTestEffect · Tutorial 55: DualTextureEffect · Tutorial 56: EnvironmentMapEffect · Tutorial 57: SkinnedEffect
- Architecture
- Graphics architecture
- Maintainer workflow
- Fix a renderer bug