Tutorial 56: EnvironmentMapEffect for Reflections
What you’ll learn
- Assigning a
TextureCubetoEnvironmentMapEffect. - Tuning
FresnelFactor,EnvironmentMapAmountandEnvironmentMapSpecular. - Layering a reflection over a diffuse texture.
Before you start — Tutorial 36: Texturing 3D Models — and note the cubemap itself is covered in depth later, in Tutorial 64: Cubemaps and Skyboxes. Requires a 3D-capable renderer such as OPENGLES3 or VULKAN; the 2D-only renderer (SDL_RENDERER) throws on 3D calls. Cube textures larger than 512 texels (or not a power of two) need GraphicsProfile::HiDef.
Cubemap-based reflection is a classic technique for making metallic, glass, or polished surfaces appear to reflect their surroundings. CNA's EnvironmentMapEffect is a built-in effect that layers a cubemap reflection on top of a diffuse texture, using the surface normal to compute a reflection vector in world space and sample the cubemap. The technique is fast, requires no ray tracing, and works on any renderer that can run the stock EnvironmentMapEffect.
EnvironmentMapEffect Overview
EnvironmentMapEffect combines a simple lit diffuse texture with a cubemap reflection layer. The reflection direction is the mirror of the view direction around the surface normal; the stock program uses it to index the cubemap sampler and blends the result over the lit diffuse colour with a Fresnel-modulated weight derived from EnvironmentMapAmount.
The effect implements the full IEffectLights interface: an ambient colour and up to three directional lights (EnableDefaultLighting() sets up XNA's rig), summed as Lambert terms per fragment — there is no Phong lobe. A specular contribution exists, but it is driven by the cubemap's alpha channel (see below), not by the light direction. Fog is also supported for distance-based atmospheric fading.
A key limitation is that the cubemap is static — it does not dynamically reflect the current scene contents unless you re-render the cubemap each frame (see Tutorial 64 for dynamic cubemaps). For typical use cases such as car chrome, armour, or shiny floor surfaces, a static sky or pre-baked environment cubemap is entirely convincing.
TextureCube (Cubemap)
A TextureCube consists of six square faces arranged to form the sides of a cube centred at the world origin:
| Face index | Axis direction | Typical content |
|---|---|---|
| 0 (+X) | Right | Scene to the right of the capture point |
| 1 (-X) | Left | Scene to the left |
| 2 (+Y) | Up | Sky, ceiling |
| 3 (-Y) | Down | Ground, floor |
| 4 (+Z) | Forward | Scene in front |
| 5 (-Z) | Back | Scene behind |
In CNA, cubemaps are loaded through ContentManager using getContentProperty().Load<TextureCube>. The type reader accepts exactly one format: a .dds file containing all six faces (or a .cnj envelope whose sourceFile points at one). There is no JSON descriptor for cubemaps, and no cross-layout or six-separate-files importer — pack the faces into a DDS with a tool such as texconv or NVIDIA Texture Tools before they reach the content directory.
// In code: load a cubemap through ContentManager
envMap_ = getContentProperty().Load<TextureCube>("skybox/env_cube");
For hand-crafted cubemaps, tools like cmftStudio or Blender's panorama-to-cubemap baker can generate the six faces from an HDR equirectangular panorama; pack that output into a single DDS as the last step. Each face should be the same square resolution (128×128 for a simple skybox, 512×512 or higher for close-up reflections).
EnvironmentMap Property
Assign the TextureCube to the effect with setEnvironmentMapProperty():
envEffect_->setEnvironmentMapProperty(&*envMap_);
The cubemap is bound to a samplerCube inside the effect's shader. Ensure your cubemap carries mipmaps — CNA does not generate them for you here, since TextureCube is read straight out of the DDS. Generate the mip chain in the packing tool (texconv -m 0 builds a full chain) so the levels are already in the file. Without mipmaps, the reflection can look aliased and noisy on surfaces seen at a sharp grazing angle. (CNA's own documentation does not state a default cube-sampler filter for every renderer, so judge the result visually on the renderer you ship.)
FresnelFactor
The Fresnel effect describes a physical phenomenon: the reflectivity of a surface increases at grazing angles. A mirror-flat pond reflects almost perfectly when viewed at a low angle (horizon), but you can see through it clearly when looking straight down. setFresnelFactorProperty(float) controls how strongly this angle-dependent variation is applied. It is an exponent, not a 0–1 blend:
- 0.0 — Fresnel disabled; the reflection is applied uniformly at all view angles with a strength equal to
EnvironmentMapAmount. - 1.0 (the default) — surfaces facing the camera reflect little and surfaces seen at grazing angles become more reflective, linearly in
1 - |N·V|. - Values above 1 concentrate the reflection towards the silhouette; the value is not bounded above.
The stock shader computes the weight per vertex (as XNA's does) and interpolates it:
// per vertex, then interpolated. The clamp reproduces Direct3D 9's saturation of COLOR outputs.
float viewAngle = dot(eyeVector, worldNormal);
float fresnel = clamp(fresnelEnabled
? pow(max(1.0 - abs(viewAngle), 0.0), fresnelFactor) * envMapAmount
: envMapAmount, 0.0, 1.0);
For a metal surface, set FresnelFactor near 0 (metals reflect at all angles). For glass or water, leave it at 1 or above (Fresnel-controlled reflectivity).
EnvironmentMapAmount
setEnvironmentMapAmountProperty(float) — the base blending weight between the diffuse texture and the cubemap reflection; the default is 1.0. At 0.0 the effect is purely diffuse. At 1.0 the object appears fully mirror-like (the diffuse texture is replaced by the reflection). Values above 1 are legal (XNA's own samples use amounts of 5) — it is the resulting per-vertex weight that is clamped to [0, 1], so a large amount with a high Fresnel exponent gives a bright rim. Most materials look best in the 0.3–0.7 range:
envEffect_->setEnvironmentMapAmountProperty(0.5f); // 50/50 diffuse/reflection blend
The Fresnel factor modulates this amount per vertex, so with Fresnel enabled the reflection is strongest where the surface turns away from the viewer; with Fresnel disabled (FresnelFactor = 0) EnvironmentMapAmount is applied uniformly.
EnvironmentMapSpecular
setEnvironmentMapSpecularProperty(Vector3) adds a specular colour on top of the reflection. It is not a light-driven highlight: the term is EnvironmentMapSpecular * cubemap.alpha, so it lights up wherever the cubemap's alpha channel is bright, which lets you paint hot spots (a sun, a window) into the environment map. The default is Vector3::Zero, which disables it; use a low-intensity neutral colour for a polished-metal look:
// Subtle white specular for polished metal
envEffect_->setEnvironmentMapSpecularProperty(Vector3(0.25f, 0.25f, 0.25f));
// Gold-tinted specular for a golden armour effect
envEffect_->setEnvironmentMapSpecularProperty(Vector3(0.4f, 0.35f, 0.1f));
The three cases side by side
These frames are from CNA's XNA oracle corpus (tools/xna-oracle/): output of the genuine Microsoft XNA 4.0 runtime, which CNA's DIRECTX9 renderer is diffed against at --tolerance 0. The same reflective quad is rendered three ways, isolating what each property contributes.
Baseline reflection — the cubemap blended in uniformly, no Fresnel, no specular. Real XNA 4.0 output; CNA’s DIRECTX9 renderer matches it pixel-for-pixel.
FresnelFactor raised — reflectivity now varies with view angle across the face. Real XNA 4.0 output; CNA’s DIRECTX9 renderer matches it pixel-for-pixel.
EnvironmentMapSpecular added — the highlight drives the surface to full white here. Real XNA 4.0 output; CNA’s DIRECTX9 renderer matches it pixel-for-pixel.
Combining with Diffuse Texture
The diffuse texture set via setTextureProperty(Texture2D*) provides the base surface colour. In the fragment shader the effect computes (simplified from the stock program):
// lightSum: up to three directional Lambert terms
vec3 litRGB = lightSum * diffuseColor.rgb + emissiveColor;
vec4 texColor = texture(diffuseTex, uv);
vec4 envSample = texture(envMap, reflect(-eyeDir, N));
vec3 baseColor = litRGB * texColor.rgb;
float combinedAlpha = diffuseColor.a * texColor.a;
vec3 rgb = mix(baseColor, envSample.rgb * combinedAlpha, fresnel) // fresnel: per-vertex weight above
+ envMapSpecular * envSample.a * combinedAlpha;
fragColor = vec4(rgb, combinedAlpha);
So DiffuseColor, EmissiveColor and the Alpha property all take part, and the alpha of the diffuse texture is preserved: you can use semi-transparent cubemap-reflected objects (e.g., glass orbs) by enabling alpha blending alongside the effect.
Complete Example: Shiny Metallic Sphere
class ReflectGame final : public Game {
std::unique_ptr<EnvironmentMapEffect> envEffect_;
Texture2D diffuseTex_;
std::optional<TextureCube> envMap_;
std::unique_ptr<VertexBuffer> sphereVB_;
std::unique_ptr<IndexBuffer> sphereIB_;
int vertexCount_ = 0; // both set by BuildSphere()
int triCount_ = 0;
float rotation_ = 0.0f;
Matrix view_, projection_; // your own camera matrices (Tutorial 34)
void LoadContent() override {
auto& gd = getGraphicsDeviceProperty();
diffuseTex_ = getContentProperty().Load<Texture2D>("textures/metal_scratched");
envMap_ = getContentProperty().Load<TextureCube>("skybox/env_cube");
envEffect_ = std::make_unique<EnvironmentMapEffect>(gd);
envEffect_->setTextureProperty(&diffuseTex_);
envEffect_->setEnvironmentMapProperty(&*envMap_);
// Reflection settings
envEffect_->setFresnelFactorProperty(0.5f);
envEffect_->setEnvironmentMapAmountProperty(0.6f);
envEffect_->setEnvironmentMapSpecularProperty(Vector3(0.3f, 0.3f, 0.3f));
// Lighting
envEffect_->setAmbientLightColorProperty(Vector3(0.2f, 0.2f, 0.25f));
// Direction is the direction the light travels (XNA convention).
DirectionalLight& key = envEffect_->getDirectionalLight0Property();
key.setEnabledProperty(true);
key.setDirectionProperty(Vector3(0.5f, -1.0f, -0.5f));
key.setDiffuseColorProperty(Vector3(1.0f, 0.95f, 0.85f));
// Generate a subdivided UV sphere
// BuildSphere fills sphereVB_/sphereIB_ and sets vertexCount_/triCount_;
// see Tutorial 51 for the mesh-generation mechanics.
BuildSphere(gd, 32, 32);
}
void Update(GameTime& gt) override {
rotation_ += static_cast<float>(gt.getElapsedGameTimeProperty().getTotalSecondsProperty()) * 0.4f;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color(20, 20, 30, 255));
Matrix world = Matrix::CreateRotationY(rotation_);
envEffect_->setWorldProperty(world);
envEffect_->setViewProperty(view_);
envEffect_->setProjectionProperty(projection_);
gd.SetVertexBuffer(sphereVB_.get());
gd.setIndicesProperty(sphereIB_.get());
for (auto& pass : envEffect_->getCurrentTechniqueProperty()->getPassesProperty()) {
pass.Apply();
gd.DrawIndexedPrimitives(
PrimitiveType::TriangleList,
0, 0, vertexCount_, 0, triCount_);
}
// No gd.Present(): Game presents in EndDraw().
}
};
Static cubemap: The environment cubemap used by EnvironmentMapEffect is pre-baked and does not update as the scene changes at runtime. Dynamic objects and moving lights will not appear in the reflection. For dynamic reflections, see Tutorial 64 (Cubemaps and Skyboxes) for the technique of rendering a live cubemap each frame (a RenderTargetCube is a TextureCube, so it can be assigned directly with setEnvironmentMapProperty()).
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- The XNA stock effects: exact semantics, worked uses and verification history — BasicEffect, AlphaTestEffect, DualTextureEffect, EnvironmentMapEffect and SkinnedEffect in CNA: defaults, formulas, ordering traps, per-vertex versus per-pixel lighting, worked uses and the defect patterns behind the current code.