Environment Lighting with Server-Side V8

In a previous post we covered creating an empty scene by writing a server-side V8 command in JavaScript. Now let’s turn on some light by adding an environment lighting setup to our empty scene. We’ll create two new commands, one to add lighting based on a spherical HDRI image and another using the built in physically based sun and sky system.

Introduction

Before starting, if you haven’t already read our post on Creating an Empty Scene with Server-side V8 please head over there and read it now as this post assumes you are familiar with what is covered there.

In this post we will create two new commands, tutorial_add_hdri.js and tutorial_add_sunsky.js. If you’re following along you can create these files and save them in the v8/examples directory of your RealityServer installation. In RealityServer you can illuminate your scene in three ways.

Here we will only cover environment lighting and save the other techniques for another post. However for many scenes and use cases environment lighting will be all you need and its very easy to use. Let’s get started.

Note also that when you enable environment lighting, that the environment will be visible where ever the camera in your scene sees the background. If you want to use different imagery for the lighting and the background you need to use the virtual backplate function (not covered in this article).

V8 Helper Classes

Like the previous example, we’ll need some helper classes to make life easier. We need Scene and Options like last time but will add three more.

const Image = require('Image');
const Texture = require('Texture');
const Mdl_function_call = require('Mdl_function_call');

We only need Image and Texture for the HDRI lighting but Mdl_function_call is needed for both.

HDRI Environment Lighting

Environment lighting with a HDRI image usually involves using a photographically captured, spherical image with High Dynamic Range information. These cover 360 degrees horizontally and 180 degrees vertically and usually look something like this.

Example Spherical HDRI – Source HDRI Haven

These images usually come in a format such as .hdr or .exr since high dynamic range data cannot be stored in a traditional JPEG or PNG image. The image will both illuminate our scene and serve as a background.

Strictly speaking you can hook up any texture to the environment even if it is not high dynamic range and you will still get something, however your renderings will lack contrast and if it is not a spherical environment you might not get the results you are expecting.

HDRI Haven is an amazing free source with lots of high quality HDRI environments you can use with RealityServer. They are adding new ones all the time. If you like what they are doing you should definitely consider supporting their work on Pateron. In this article we’ll use a HDRI that already comes with RealityServer for our testing.

Command Definition

Let’s define the interface for our command.

module.exports.command = {
    name: 'tutorial_add_hdri',
    description: 'Adds a HDRI lighting environment to an existing scene.',
    groups: ['tutorial', 'javascript'],
    arguments: {
        scene_name: {
            description: 'The name of the existing scene to add the HDRI lighting to.',
            type: 'String'
        },
        hdri_filename: {
            description: 'The filename of the HDRI image to use for the environment.',
            type: 'String'
        },
        hdri_intensity: {
            description: 'Multiplier for the values within the HDRI image.',
            type: 'Float32',
            default: 1.0
        }
    }
}

This is the same structure scene in the empty scene example. Here though we’ll take three parameters. Firstly, we’ll get the name of the existing scene we want to add the HDRI environment to. We also want a filename for the HDRI image and a value for the intensity so we can brighten our environment if required. Note that this command doesn’t return anything so the definition has no returns property.

The observant among you will notice something not shown in the previous example, a default value. Parameters in V8 commands can specify a default value (in this case we give one for hdri_intensity). If a default is specified the command can be called without specifying that value and the default will be used. This is useful for making optional parameters.

Execution

To start building up the execute function we will fetch the existing scene data so we can modify it.

module.exports.command = {
    ...
    execute: function({scene_name, hdri_filename, hdri_intensity}) {
        // Fetch the existing scene data to work with
        const scene = new Scene(scene_name);
        const options = scene.options;
        ...
    }
};

Here, rather than create a new scene as we did in the previous example, we initialise our scene object by retrieving an existing scene from the RealityServer database (for example one made with the command for creating empty scenes, though it could also be one imported using Scene.import). This is done by omitting the second parameter of the Scene constructor (this pattern holds for all helpers derived from Element).

Once we have the scene we can grab its Options element which will give us a pre-populated Options object, saving us having to retrieve it from the database by name. We need the options later to tell RealityServer what to use for the environment. Now we’ll continue adding functionality to our execute function.

let hdri_image = new Image(`${scene.name}_environment_image`, true);

This makes an image element in the database, for now with no actual image data. Note the second parameter which forces the element to be created. An image without any data isn’t much good so let’s load our HDRI.

hdri_image.filename = hdri_filename;

Setting the filename property of the Image object causes the file to be loaded. The file will be searched for in any texture paths you have configured for RealityServer and also within content_root. Image elements are not used directly in RealityServer so we need to wrap it in a Texture element.

let hdri_texture = new Texture(`${scene.name}_environment_texture`, true);
hdri_texture.image = hdri_image;

This creates a new Texture element in the database and then sets its image property which associates the image data with the texture. Now we can finally use the texture as an environment.

Scene.import_elements('${shader}/base.mdl');

let environment = Mdl_function_call.create(`${scene.name}_environment_function`,
    'mdl::base::environment_spherical', {
        texture: hdri_texture
    }
);

To setup the environment we’ll need to use an MDL function called environment_spherical. This is contained in the built in base module. Even though this file doesn’t exist on disk we have to load it to make the functions within the module available. We can import things using the static import_elements method on the Scene object.

We then call the static method create on Mdl_function_call to create a new MDL function call (an instance of an MDL function definition). The function we need is environment_spherical. This takes one argument texture which references the texture to use as the environment. So we pass in the texture element we have just made. It’s ready to use but isn’t hooked up yet, for that we need to set some attributes on our Options element for the scene.

options.attributes.set('environment_function', environment.name, 'Ref');
options.attributes.set('environment_function_intensity', hdri_intensity, 'Float32');

The environment_function attribute tells the renderer which MDL function to use when looking up the environment, while the environment_function_intensity sets a multiplier for the brightness of the environment. Here we pass in our hdri_intensity argument to set that. For the environment_function we need to get the name property of the environment texture since this attribute is of type Ref, which is a reference to another element in the database, which is made by name.

The command should now actually do something if you call it. Here is a quick test JSON-RPC command set you can HTTP POST to RealityServer to verify.

[
    {"jsonrpc": "2.0", "method": "create_scope", "params": {
        "scope_name": "tutorial_scope"
    }, "id": 1},
    {"jsonrpc": "2.0", "method": "use_scope", "params": {
        "scope_name": "tutorial_scope"
    }, "id": 2},
    {"jsonrpc": "2.0", "method": "tutorial_create_empty_scene", "params": {
        "scene_name": "tutorial_scene"
    }, "id": 3},
    {"jsonrpc": "2.0", "method": "tutorial_add_hdri", "params": {
        "scene_name": "tutorial_scene",
        "hdri_filename": "studio_grid.exr",
        "hdri_intensity": 1.0
    }, "id": 4},
    {"jsonrpc": "2.0", "method": "render", "params": {
        "scene_name": "tutorial_scene"
    }, "id": 5},
    {"jsonrpc": "2.0", "method": "delete_scope", "params": {
        "scope_name": "tutorial_scope"
    }, "id": 6}
]

If you run this command sequence you should get an image similar to the one on the right. What you are seeing is the background using the HDRI. We can’t see the effect of lighting yet as we don’t have any objects in our scene.

There is no tone-mapping or other camera modifications done and no objects in the scene, we’ll cover those in a future article. However if you continue to add objects to this scene they will be illuminated by the new environment.

This completes the first command of this article and allows you to add a simple HDRI environment to your scenes. There are many options available in RealityServer to control the shape of the environment dome, add ground planes, and re-orient it. You can find more details in the Iray Programmers Guide in the RealityServer Document Center.

Image with HDRI Environment

Physically Based Daylight Environment

In addition to texture or MDL function based environments, RealityServer also has the ability use a built in physically based daylight system. Rather than work from a texture, this dynamically evaluates the sun and sky and can be changed interactively. Using this is just a matter of attaching a different MDL function to the environment_function attribute.

Command Definition

We’ll define a slightly different interface for this command. Like the previous one we’ll also take in the name of an existing scene but instead of the hdri parameters there is a parameter to control the sun direction and one to control the intensity of the daylight system. Here is the command definition.

module.exports.command = {
    name: 'tutorial_add_sunsky',
    description: 'Adds a physically based sun and sky environment to an existing scene.',
    groups: ['tutorial', 'javascript'],
    arguments: {
        scene_name: {
            description: 'The name of the existing scene to add the sun and sky lighting to.',
            type: 'String'
        },
        sun_direction: {
            description: 'Direction vector from the origin to the direction of the sun.',
            type: 'Float32<3>',
            default: { x: 0.0, y: 1.0, z: 0.0 }
        },
        multiplier: {
            description: 'Multipler to scale the intensity of the sun and sky system.',
            type: 'Float32',
            default: 0.10132
        }
    }
}

For the sun direction, you can just directly specify a vector but if you have the RealityServer Extras package (which is distributed with RealityServer) there is a set_sun_position command which you can use to set this from time of day, date and geographic location instead.

Execution

We start the same as the previous command, by grabbing the scene and then the options from the scene.

module.exports.command = {
    ...
    execute: function({scene_name, sun_direction, multiplier}) {
        // Fetch the existing scene data to work with
        const scene = new Scene(scene_name);
        const options = scene.options;
        ...
    }
};

After that there are fewer steps involved since we don’t need an image or texture. We just need to create the MDL function for the sun and sky.

Scene.import_elements('${shader}/base.mdl');

let environment = Mdl_function_call.create(`${scene.name}_environment_function`,
    'mdl::base::sun_and_sky', {
        on: true,
        multiplier: multiplier,
        rgb_unit_conversion: { r: 1.0, g: 1.0, b: 1.0 },
        haze: 0.5,
        redblueshift: 0.0,
        saturation: 1.0,
        horizon_height: 0.001,
        horizon_blur: 0.1,
        ground_color: { r: 0.4, g: 0.4, b: 0.4 },
        night_color: { r: 0.0, g: 0.0, b: 0.0 },
        sun_direction: sun_direction,
        sun_disk_intensity: 1.0,
        sun_disk_scale: 1.0,
        sun_glow_intensity: 1.0,
        y_is_up: true,
        flags: 0,
        physically_scaled_sun: true
    }
);

As before we first import the base MDL functions then create an MDL function call. You can see the range of parameters available for the sun_and_sky function is quite large. Most are not needed but the above setup will produce physically accurate lighting values (assuming your multiplier is set to 1.0). Now like before we attach the function to the options.

options.attributes.set('environment_function', environment.name, 'Ref');
options.attributes.set('environment_function_intensity', 1.0, 'Float32');

For this case we set the environment_function_intensity to 1.0 since the brightness is directly controlled on the environment function. Here is a quick test JSON-RPC command sequence to render an image with this environment.

[
    {"jsonrpc": "2.0", "method": "create_scope", "params": {
        "scope_name": "tutorial_scope"
    }, "id": 1},
    {"jsonrpc": "2.0", "method": "use_scope", "params": {
        "scope_name": "tutorial_scope"
    }, "id": 2},
    {"jsonrpc": "2.0", "method": "tutorial_create_empty_scene", "params": {
        "scene_name": "tutorial_scene"
    }, "id": 3},
    {"jsonrpc": "2.0", "method": "tutorial_add_sunsky", "params": {
        "scene_name": "tutorial_scene",
        "sun_direction": { "x": 0.0, "y": 0.05, "z": -0.2 },
        "multiplier": 0.000001
    }, "id": 4},
    {"jsonrpc": "2.0", "method": "render", "params": {
        "scene_name": "tutorial_scene"
    }, "id": 5},
    {"jsonrpc": "2.0", "method": "delete_scope", "params": {
        "scope_name": "tutorial_scope"
    }, "id": 6}
]

If we run this sequence we get something similar to the image on the right. Note we used a very small multiplier in this case. This is because we don’t have tone-mapping turned on yet and daylight is extremely bright.

To learn more about the sun and sky system see the documentation for the MDL Base Module in the RealityServer Document Center. The chapter on Physically Plausible Scene Setup in the Iray Programmers guide is also very useful when setting up realistic environments.

The big advantage of the built in sun and sky system is you can interactively change the parameters of the MDL function, such as sun_direction without reprocessing the environment. Note also that there is an alternative sun and sky system function called perez_sun_and_sky . This can be useful for daylight analysis as it is an industry accepted model.

Image with a Sun/Sky Environment

Summary

In this post we have covered how to add two types of environment lighting to your RealityServer scene. Right now we’re still just making an empty scene and not doing anything particularly useful but in our next post we’ll show how to add objects to your scene and then how to adjust your camera settings. If you’re having trouble getting started with RealityServer be sure to contact us.

You can download the example commands shown in this post below. They have been extensively commented so you can follow along in the code.

Articles Tutorials
Get in Touch