Tutorial 56: EnvironmentMapEffect for Reflections

CNA Tutorials  ·  Built-in Effects

ℹ

What you’ll learn

  • Assigning a TextureCube to EnvironmentMapEffect.
  • Tuning FresnelFactor, EnvironmentMapAmount and EnvironmentMapSpecular.
  • 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 indexAxis directionTypical content
0 (+X)RightScene 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)ForwardScene 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.

A flat tan-brown square on a cornflower-blue background, evenly lit with no variation across its surface.

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.

A brown square shading from bright orange-brown at the top to near-black at the bottom, on cornflower blue.

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.

A solid white square on cornflower blue; the specular term has saturated the whole surface.

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()).