API

Volumetric Light Scattering Post Process

The Volumetric LightScattering post-process

BABYLON.VolumetricLightScatteringPostProcess is a post-process that computes light scattering based on a light source mesh.

How to use it? Easy!

import { Texture } from "@babylonjs/core/Materials/Textures/texture";
import { VolumetricLightScatteringPostProcess } from "@babylonjs/core/PostProcesses/volumetricLightScatteringPostProcess";
const vls = new VolumetricLightScatteringPostProcess("vls", 1.0, camera, lightSourceMesh, samplesNum, Texture.BILINEAR_SAMPLINGMODE, engine, false);

_ Parameters _

  • name {string} - The post-process name.
  • ratio {any} - The size of the post-process and/or internal pass (0.5 means that your post-process will have a width = canvas.width * 0.5 and a height = canvas.height * 0.5.)
  • camera {BABYLON.Camera} - The camera that the post-process will be attached to.
  • lightSourceMesh {BABYLON.Mesh} - The mesh used as a light source, to create the light scattering effect (for example, a billboard with its texture simulating the sun.)
  • samplesNum {number} - The post-process quality. Default is 100.
  • samplingMode {number} - The post-process filtering mode.
  • engine {BABYLON.Engine} - The Babylon engine.
  • reusable {boolean} - If the post-process is reusable.
  • scene {BABYLON.Scene} - If the camera parameter is null (adding the post-process in a rendering pipeline), the scene is needed to configure the internal pass.

The lightSourceMesh is a mesh that will contain the light color, typically a billboard with a diffuse texture. If your light source is coming from the floor, you can use the floor/ground mesh to compute the light scattering effect.

Note: The light source mesh can be null. This causes a default lightSourceMesh to be created for you as a billboard.

To create the default mesh before the post-process, there is a static method that returns a billboard as the default:

import { VolumetricLightScatteringPostProcess } from "@babylonjs/core/PostProcesses/volumetricLightScatteringPostProcess";
const defaultMesh = VolumetricLightScatteringPostProcess.CreateDefaultMesh("meshName", scene);

You can access and modify the mesh using:

const mesh = vls.mesh;

By default, the post-process computes light scattering using the internal mesh position. You can modify it and set a custom position using the following code (typically for the floor as the internal mesh):

import { Vector3 } from "@babylonjs/core/Maths/math.vector";
vls.useCustomMeshPosition = true;
vls.setCustomMeshPosition(new Vector3(5.0, 0.0, 5.0));

Warning: If the custom light position is too far from the light source, the result will be distorted.

You can access the custom position using:

const position = vls.getCustomMeshPosition();

To customize the light scattering, you can modify the vertical direction of the light rays. If invert is set to true, the rays will go downward. If invert is set to false, the rays will go upward.

vls.invert = true;

To optimize performance, you can customize the rendering quality. In fact, this post-process uses an internal pass (render target texture) that helps the post-process compute the light scattering effect. Of course, you can compute the pass at a lower ratio, like this:

import { Texture } from "@babylonjs/core/Materials/Textures/texture";
import { VolumetricLightScatteringPostProcess } from "@babylonjs/core/PostProcesses/volumetricLightScatteringPostProcess";
const vls = new VolumetricLightScatteringPostProcess("vls", { postProcessRatio: 1.0, passRatio: 0.5 }, camera, lightSourceMesh, 75, Texture.BILINEAR_SAMPLINGMODE, engine, false);

vls.useDiffuseColor is used to force rendering the diffuse color of the light source mesh instead of its diffuse texture.

  • If useDiffuseColor is true or material.diffuseTexture is undefined, use the diffuse color

  • If useDiffuseColor is false and material.diffuseTexture is not undefined, use diffuse texture

  • If useDiffuseColor is false and material.diffuseTexture is undefined, use diffuse color

Using material.diffuseColor instead of material.diffuseTexture (the default) for the light's color:

import { Color3 } from "@babylonjs/core/Maths/math.color";
vls.useDiffuseColor = true; // False as default
vls.mesh.material.diffuseColor = new Color3(0.0, 1.0, 0.0);

Using the material.diffuseTexture for the light's color:

import { Texture } from "@babylonjs/core/Materials/Textures/texture";
vls.useDiffuseColor = false; // False as default
vls.mesh.material.diffuseTexture= new Texture(...);

And now, it's time to play

Feel free to explore some examples of Volumetric LightScattering in the Playground:

  • Basic Example
  • Spherical Harmonics as Source
  • VLS through CSG-created slots

Have fun!

The Volumetric Lighting Task

Starting with Babylon 9.0, you can use a new volumetric lighting effect, but it is only available with frame graphs: the FrameGraphVolumetricLightingTask.

The implementation is based on the article “Participating media using extruded light volumes” that you can find in GPU Zen 1. You can also find information in the GDC slides.

The FrameGraphVolumetricLightingTask task renders a mesh using the technique described in the links above. In general, you will want this mesh to represent the volume that a light can reach in your scene: use the FrameGraphLightingVolumeTask to generate such a mesh:

import { FrameGraphLightingVolumeTask } from "@babylonjs/core/FrameGraph/Tasks/Misc/lightingVolumeTask";
import { FrameGraphVolumetricLightingTask } from "@babylonjs/core/FrameGraph/Tasks/PostProcesses/volumetricLightingTask";
import { Color3 } from "@babylonjs/core/Maths/math.color";
import { Vector3 } from "@babylonjs/core/Maths/math.vector";
// Create first a shadowGeneratorTask and a renderTask instance [...]
// Then
const lightingVolumeTask = new FrameGraphLightingVolumeTask("lightingVolume", frameGraph);
lightingVolumeTask.shadowGenerator = shadowGeneratorTask;
const lightVolume = lightingVolumeTask.lightingVolume;
lightVolume.frequency = isWebGPU ? 1 : 4;
lightVolume.tesselation = isWebGPU ? 1024 : 256;
frameGraph.addTask(lightingVolumeTask);
const volumetricLightingTask = new FrameGraphVolumetricLightingTask("volumetricLighting", frameGraph, true /*enableExtinction*/);
volumetricLightingTask.targetTexture = renderTask.outputTexture;
volumetricLightingTask.depthTexture = renderTask.outputDepthTexture;
volumetricLightingTask.camera = camera;
volumetricLightingTask.lightingVolumeMesh = lightingVolumeTask.outputMeshLightingVolume;
volumetricLightingTask.light = light;
volumetricLightingTask.lightPower = new Color3(0.8, 0.8, 0.8);
volumetricLightingTask.extinction = new Vector3(0.01, 0.01, 0.03);
volumetricLightingTask.phaseG = 0.05;
frameGraph.addTask(volumetricLightingTask);

This effect works best in WebGPU because we can use a compute shader to update the lighting volume, whereas in WebGL, we have to read back the shadow map texture and update the volume on the CPU side.

That's why, in the code above, we reduce the frequency of volume updates when using WebGL (4 means we update every 4 frames) and use a lower tessellation value (i.e., fewer triangles) for the mesh to improve performance.

Please refer to the FrameGraphVolumetricLightingTask page for detailed information on task parameters and PG examples.

Volumetric lighting task