Skip to content
This repository has been archived by the owner on Dec 14, 2021. It is now read-only.

Lighting

David Lenaerts edited this page Oct 6, 2018 · 3 revisions

Lights are an important aspect in a scene, allowing objects to be shaded. All lights are added to a scene as Components belonging to an Entity.

Lighting models

Lighting in Helix is separated from geometry code in Helix. This allows you to use the same geometry code without having to worry which kind of lighting model you wish to apply. There are some default lighting models supplied through the HX.LightingModel namespace:

  • Unlit: No lighting is applied (this is the default lighting mode unless something else is assigned)
  • GGX: A physically plausible lighting model
  • BlinnPhong: Traditional Blinn-Phong lighting adapted to be energy-conserving.

Helix allows replacing the default lighting model it uses through its initialization options parameter. Any material that will be created will use this model by default. When initializing Helix, provide an InitOptions object with the defaultLightingModel property set:

var options = new HX.InitOptions();
options.defaultLightingModel = HX.LightingModel.GGX;
HX.init(canvas, options);

Direct lighting

If you'll set the lighting model to a material, you'll notice that the object turns black. There are of course no lights in the scene. At this point, you can add 3 types of direct lights:

  • HX.DirectionalLight: a light "at infinity", where all light rays are parallel. Useful for sunlight.
  • HX.PointLight: A local light with a location in space.
  • HX.SpotLight: A local light with a conical range.

Lighting in Helix is following a physically plausible model, which means the intensity of the point/spot light decreases by distance (the inverse square law). However, for performance reasons it also accepts a "radius" property that adds an extra linear falloff, so the effective range of the light (and thus the objects and pixels it affects) can be reduced.

Apart from the regular transform properties of any scene graph node, lights have a color and intensity property. In the case of directional lights, properties direction, and castShadows are also available.

Shadows

Direct lights can cast shadows on the scene. To do so, simply set the castShadows property.

var directionalLight = new HX.DirectionalLight();
directionalLight.castShadows = true;

var dirLightEntity = new HX.Entity(directionalLight);
dirLightEntity.lookAt(new HX.Float4(-1, -1, -1));

scene.attach(dirLightEntity);

By default, Helix uses simple "hard" shadows. You can improve the appearance of the shadows – at the cost of performance – using a so-called "shadow filter". This is an object that can be passed along during Helix initialisation:

var options = new HX.InitOptions();
options.defaultLightingModel = HX.LightingModel.GGX;

var filter = new HX.PCFShadowFilter();
filter.numSamples = 8;
options.shadowFilter = filter;
HX.init(canvas, options);

Other filters are available, but these are too experimental at this point so I won't mention them here.

Another way to change the quality of shadows is to change the resolution of the shadow map, which is a property of Renderer:

renderer.shadowMapSize = 2048;

By default, the size is 2048.

A final way to improve quality for directional lights is to introduce shadow cascades. This will cause the view frustum to be split into sections and each will get its own shadow map. This property is again set during initialisation:

var options = new HX.InitOptions();
options.defaultLightingModel = HX.LightingModel.GGX;
options.numShadowCascades = 3;
HX.init(canvas, options);

The maximum value is 4.

You may wonder why the filter and cascades are set in the initialisation phase, and some on the light or renderer itself and thus can be changed at will. Some properties affect the lighting shader code, and as such need to apply to all lights using this shader.

Helix uses a single shadow map for all lights in a scene, which it uses it as a texture atlas.

Global Illumination

Global illumination is light not coming directly from light sources, but coming from objects reflecting and scattering light. This is hard to calculate correctly, but Helix provides some approximations:

Ambient light

An ambient light is a simple light that sets a minimum amount of light in the whole scene:

var ambientLight = new HX.AmbientLight();
ambientLight.color = 0xeeeeff;
ambientLight.intensity = 0.3;
scene.attach(new HX.Entity(ambientLight));

It basically just brightens things up a bit overall.

Light probes

Light probes are essentially cube maps containing lighting information for all directions in the scene. For specular reflections, these are basically identical to environment maps. However, if the EXT_shader_texture_lod extension is supported, the mipmaps of the texture can be used to contain the reflection maps for different roughness values, useful for glossy reflections. Diffuse light is approximated by L2 Spherical Harmonics, allowing a spherical function to be represented by 9 floats for each colour channel.

The authoring for these types of assets is beyond the scope of this article, but check out Knald's Lys for a tool that can generate specular mip chains kinds and SH files (as .ash files) from HDR environment maps. The Helix Tools also contains a little web-based tool to generate SH files: https://derschmale.github.io/helixjs-tools/sh-generator/build/index.html

Loading cube maps in Helix can be done by creating little JSON HCM files:

{
    "version": "0.1",
    "files": {
        "negX": "radiance/negX.jpg",
        "posX": "radiance/posX.jpg",
        "negY": "radiance/negY.jpg",
        "posY": "radiance/posY.jpg",
        "negZ": "radiance/negZ.jpg",
        "posZ": "radiance/posZ.jpg"
    },
    "loadMips": false
}

And then simply using the HX.AssetLoader or HX.AssetLibrary:

assetLibrary.queueAsset("irradiance-sh", "irradiance.ash", HX.AssetLibrary.Type.ASSET, HX.ASH);
assetLibrary.queueAsset("specular-map", "specular.hcm", HX.AssetLibrary.Type.ASSET, HX.HCM);

// on load complete:

var skyboxDiffuseSH = assetLibrary.get("irradiance-sh");
var skyboxSpecularTexture = assetLibrary.get("specular-map");

If you want to load an entire mip-chain with custom glossiness settings, store each miplevel in a subdirectory with the miplevel as name, for example:

specular/
    0/
        negX.png
        negY.png
        negZ.png
        posX.png
        posY.png
        posZ.png
    1/
        negX.png
        ...
    2/
        ...
    ...

Then, create the HCM file as such:

{
    "version": "0.1",
    "files": {
        "negX": "specular/%m/negX.png",
        "posX": "specular/%m/posX.png",
        "negY": "specular/%m/negY.png",
        "posY": "specular/%m/posY.png",
        "negZ": "specular/%m/negZ.png",
        "posZ": "specular/%m/posZ.png"
    },
    "loadMips": true
}

This will replace the %m for each mip level.

Once the cube maps have been loaded, simply create the light probe as such:

var lightProbe = new HX.LightProbe(skyboxDiffuseSH , skyboxSpecularTexture);
scene.attach(new HX.Entity(lightProbe));

Alternatively, you can also load DDS cube maps or HDR files, but their filesizes can get quite high.

If you wish to use multiple probes, you need to change the InitOptions.maxDiffuseProbes property when initializing the engine.

Fixed light set-up

If your object and lighting is static, or if you always know which lights affect which objects, you can set the material's fixedLights property:

var light1 = new HX.DirectionalLight();
var light2 = new HX.PointLight();
var lightEntity1 = new HX.Entity(light1);
var lightEntity2 = new HX.Entity(light2);
// set the direction
lightEntity1.lookAt(new HX.Float4(-1, -1, -1));
scene.attach(lightEntity1);
scene.attach(lightEntity2);
material.fixedLights = [ light1, light2 ];

This prevents the engine from having to figure out which lights affect the object, providing a potentially immense performance improvement. In most cases, you'll be wanting to use this method.

Note that you need to assign the light Components, not the Entities!

If you don't care about selecting lights, and just want to assign all lights to all materials, Helix provides a way to automatically assign and update the fixedLights property in the form of the system FixedLightsSystem:

scene.startSystem(new HX.FixedLightsSystem());

When started, all materials in the scene will have every light assigned to them. Removing and updating lights updates them. Since this will need to recompile the shaders and this is quite a slow operation, it may not update all the materials in the same frame but delay updates in order to keep more consistent frame rates.

Ambient occlusion

Ambient occlusion is a technique that calculates how much of the indirect light reaches a certain point, depending on scene geometry. Helix provides some screen-space solutions that get applied to both ambient lights as well as light probes. For performance reasons, the effect needs to be assigned on startup:

var options = new HX.InitOptions();

var ssao = new HX.SSAO(16);
ssao.strength = 2.0;
ssao.sampleRadius = 1.0;
ssao.fallOffDistance = 2.0;
options.ambientOcclusion = ssao;

HX.init(canvas, options);

sampleRadius is the world space size to search for occluders. Since it does so in screen space, the area it searches in might also contain objects that are actually outside this radius. fallOffDistance is the distance that determines how for objects can be to still be included.

Another ambient occlusion technique that's available is HX.HBAO, which is a slightly incorrect but faster implementation than the original NVidia approach.