Tutorial 56: EnvironmentMapEffect for Reflections
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 is supported on all shader-capable backends.
EnvironmentMapEffect Overview
EnvironmentMapEffect implements a per-vertex Phong lighting model combined with a cubemap reflection layer. In the vertex shader, the reflection direction is computed as the mirror of the view direction around the surface normal. This direction is passed to the fragment shader as a varying and used to index into the cubemap sampler. The result is then blended with the diffuse texture colour using the EnvironmentMapAmount scalar.
The effect supports one directional light (inherited from the built-in BasicEffect lighting model), ambient light, and an optional specular highlight that is added on top of the reflection. 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 via the content pipeline using Content.Load<TextureCube>. The importer expects either a pre-packed cross or strip image, or six individual face images referenced in a .texcube.json descriptor. A typical descriptor looks like:
// In code: load a cubemap from content pipeline
envMap_ = Content.Load<TextureCube>("skybox/env_cube");
For hand-crafted cubemaps, tools like cmftStudio or Blender's panorama-to-cubemap baker can generate the six face images from an HDR equirectangular panorama. 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 setEnvironmentMap():
envEffect_->setEnvironmentMap(envMap_.get());
The cubemap is bound to a samplerCube uniform inside the effect's GLSL shader. The sampler uses trilinear filtering with mipmaps by default. Ensure your cubemap TextureCube object has mipmaps generated — the content pipeline generates them automatically when the source descriptor specifies "generateMipmaps": true. Without mipmaps, the reflection will appear aliased and noisy on surfaces that are at a sharp grazing angle.
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. setFresnelFactor(float) controls how strongly this angle-dependent variation is applied:
- 0.0 — Fresnel disabled; the reflection is applied uniformly at all view angles with a strength equal to
EnvironmentMapAmount. - 1.0 — Full Fresnel: surfaces facing the camera are more diffuse, surfaces seen at grazing angles become more reflective.
- Values between 0 and 1 blend between the two extremes.
The internal formula (approximating the Schlick Fresnel) is:
float fresnelWeight = pow(1.0 - abs(dot(N, V)), u_fresnelFactor * 5.0);
float reflectWeight = u_envMapAmount + fresnelWeight * (1.0 - u_envMapAmount);
For a metal surface, set FresnelFactor near 0 (metals reflect at all angles). For glass or water, set it near 1 (Fresnel-controlled reflectivity).
EnvironmentMapAmount
setEnvironmentMapAmount(float) — a scalar in [0, 1] that sets the base blending weight between the diffuse texture and the cubemap reflection. 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). Most materials look best in the 0.3–0.7 range:
envEffect_->setEnvironmentMapAmount(0.5f); // 50/50 diffuse/reflection blend
The Fresnel factor modulates this amount per-fragment, so EnvironmentMapAmount acts as the base (face-on) reflection strength while Fresnel pushes grazing pixels higher.
EnvironmentMapSpecular
setEnvironmentMapSpecular(Vector3) adds a specular highlight colour on top of the reflection. Unlike the cubemap reflection (which represents the mirror image of the environment), the specular highlight is a stylised Blinn-Phong lobe aimed at making the surface appear shiny under the directional light. Set it to Vector3::Zero to disable it, or to a low-intensity neutral colour for a polished-metal look:
// Subtle white specular for polished metal
envEffect_->setEnvironmentMapSpecular(Vector3(0.25f, 0.25f, 0.25f));
// Gold-tinted specular for a golden armour effect
envEffect_->setEnvironmentMapSpecular(Vector3(0.4f, 0.35f, 0.1f));
Combining with Diffuse Texture
The diffuse texture set via setTexture(Texture2D*) provides the base surface colour. In the fragment shader the effect computes:
vec4 diffuse = texture(u_diffuse, v_texcoord) * u_diffuseColor;
vec3 envSample = texture(u_envMap, v_reflectDir).rgb;
float reflWeight = /* Fresnel-modulated envMapAmount */;
vec3 color = mix(diffuse.rgb, envSample, reflWeight)
+ u_envMapSpecular * specularIntensity;
fragColor = vec4(color, diffuse.a);
The alpha channel of the diffuse texture is preserved, so 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_;
std::unique_ptr<Texture2D> diffuseTex_;
std::unique_ptr<TextureCube> envMap_;
std::unique_ptr<VertexBuffer> sphereVB_;
std::unique_ptr<IndexBuffer> sphereIB_;
int triCount_ = 0;
float rotation_ = 0.0f;
Camera camera_;
void LoadContent() override {
auto& gd = getGraphicsDeviceProperty();
diffuseTex_ = Content.Load<Texture2D>("textures/metal_scratched");
envMap_ = Content.Load<TextureCube>("skybox/env_cube");
envEffect_ = std::make_unique<EnvironmentMapEffect>(gd);
envEffect_->setTexture(diffuseTex_.get());
envEffect_->setEnvironmentMap(envMap_.get());
// Reflection settings
envEffect_->setFresnelFactor(0.5f);
envEffect_->setEnvironmentMapAmount(0.6f);
envEffect_->setEnvironmentMapSpecular(Vector3(0.3f, 0.3f, 0.3f));
// Lighting
envEffect_->setAmbientLightColor(Vector3(0.2f, 0.2f, 0.25f));
envEffect_->DirectionalLight0.setEnabled(true);
envEffect_->DirectionalLight0.setDirection(Vector3(0.5f, -1.0f, -0.5f));
envEffect_->DirectionalLight0.setDiffuseColor(Vector3(1.0f, 0.95f, 0.85f));
// Generate a subdivided UV sphere
BuildSphere(gd, 32, 32);
}
void Update(const GameTime& gt) override {
rotation_ += (float)gt.getElapsedGameTime().TotalSeconds() * 0.4f;
}
void Draw(const GameTime&) override {
auto& gd = getGraphicsDeviceProperty();
gd.Clear(Color(20, 20, 30, 255));
Matrix world = Matrix::CreateRotationY(rotation_);
envEffect_->setWorld(world);
envEffect_->setView(camera_.View());
envEffect_->setProjection(camera_.Projection());
gd.setVertexBuffer(*sphereVB_);
gd.setIndexBuffer(*sphereIB_);
for (auto& pass : envEffect_->getCurrentTechnique().Passes) {
pass.Apply();
gd.DrawIndexedPrimitives(
PrimitiveType::TriangleList, 0, 0, triCount_);
}
gd.Present();
}
};
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.