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 startTutorial 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 thirteen 2D-only renderers (SDL_RENDERER, DIRECT2D, CANVAS, HTML_DOM, SKIA, BLEND2D, FREEDIRECT, DIRECTX1, GDI, SVG_DOM, OPENVG, NANOVG, PIXIJS) throw on 3D calls.

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 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 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 uniform inside the effect's GLSL shader. The sampler uses trilinear filtering with mipmaps by default. 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 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. setFresnelFactorProperty(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

setEnvironmentMapAmountProperty(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_->setEnvironmentMapAmountProperty(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

setEnvironmentMapSpecularProperty(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_->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 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 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 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:

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_;
    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;
    Camera camera_;

    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));
        envEffect_->DirectionalLight0.setEnabledProperty(true);
        envEffect_->DirectionalLight0.setDirectionProperty(Vector3(0.5f, -1.0f, -0.5f));
        envEffect_->DirectionalLight0.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_ += (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(camera_.View());
        envEffect_->setProjectionProperty(camera_.Projection());

        gd.SetVertexBuffer(sphereVB_.get());
        gd.SetIndexBuffer(sphereIB_.get());
        for (auto& pass : envEffect_->getCurrentTechniqueProperty()->getPassesProperty()) {
            pass.Apply();
            gd.DrawIndexedPrimitives(
                PrimitiveType::TriangleList,
                0, 0, vertexCount_, 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.