Skip to content

Shadow

Shadows add a certain degree of depth and realism to scenes and models, because they can show the scale and position of objects, making the objects in the scene look more three-dimensional. In the engine, light sources can cast the shadows of objects onto other parts of themselves or nearby scenes. These shadows can be real-time or static.

Configuring Shadows

Shadows are implemented through light sources. Currently in the engine, DirectLight, PointLight, and SpotLight can all produce shadows. When you want to render a shadow effect, just set the castShadow property of the corresponding light to true.

ts
let lightObj = new Object3D();
// Set the light position
lightObj.x = 0;
lightObj.y = 0;
lightObj.z = 0;
// Set the light angle, rotating 45 degrees around the X axis
lightObj.rotationX = 45;
lightObj.rotationY = 0;
lightObj.rotationZ = 0;
// Add a direct light component and enable shadows
let lc = lightObj.addComponent(DirectLight);
lc.castShadow = true; // false by default
lc.intensity = 5;
scene.addChild(lightObj);

To see the shadow effect, we also need an object that produces the shadow and an object that receives the shadow. If you want the light shining on a certain object to produce a shadow effect, you need to add a MeshRenderer component to the object and set the component's castShadow property to true.

ts
// Create a box to produce shadows
let castShadowObj = new Object3D();
let mr1 = castShadowObj.addComponent(MeshRenderer);
mr1.geometry = new BoxGeometry();
mr1.material = new LitMaterial();
mr1.castShadow = true
scene.addChild(castShadowObj);

Then you need to add a MeshRenderer component to the object that receives the shadow, and set the component's receiveShadow property to true.

ts
// Create a plane to receive shadows
let receiveShadowObj = new Object3D();
let mr2 = receiveShadowObj.addComponent(MeshRenderer);
mr2.geometry  = new PlaneGeometry(1000,1000);
mr2.material =new LitMaterial();
mr2.receiveShadow = true;
scene.addChild(receiveShadowObj);

In this way, you can see in the scene a box that, under the illumination of the direct light, produces a shadow and casts it onto the plane:

Direct Light Shadow


WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, Camera3D, Vector3, AtmosphericComponent, View3D, LitMaterial, BoxGeometry, MeshRenderer, UnLitMaterial, SphereGeometry, DirectLight, PointLight, SpotLight, HoverCameraController, PlaneGeometry, Color } from '@orillusion/core';

let engine = await Engine3D.init({
    canvasConfig: { devicePixelRatio: 1 },
    // shadow setting
    setting: {
        shadow: {
            autoUpdate: true,
            shadowBound: 100,
            type: 'HARD'
        }
    }
});
let scene3D: Scene3D = new Scene3D();
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(60, engine.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(-45, -45, 100, new Vector3(0, 0, 0));
scene3D.addChild(cameraObj);

//DirectLight
{
    let obj = new Object3D();
    obj.y = 50;
    obj.rotationX = 45;
    obj.rotationY = 0;
    obj.rotationZ = 0;
    let light = obj.addComponent(DirectLight);
    scene3D.addChild(obj);
    // enable light shadow
    light.castShadow = true;
    light.intensity = 30;
    light.shadowBias = 0.01;
    scene3D.addChild(obj);
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    castShadowObj.rotationY = 45;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(1, 0, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a plane to receive shadow
{
    let receiveShadowObj = new Object3D();
    let mr = receiveShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(2000, 1, 2000);
    mr.material = new LitMaterial();
    mr.receiveShadow = true;
    scene3D.addChild(receiveShadowObj);
}
// create a view with target scene and camera
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
// start render
engine.startRenderView(view);

Point Light Shadow


WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, Camera3D, Vector3, AtmosphericComponent, View3D, LitMaterial, BoxGeometry, MeshRenderer, UnLitMaterial, SphereGeometry, DirectLight, PointLight, SpotLight, HoverCameraController, PlaneGeometry, Color } from '@orillusion/core';

let engine = await Engine3D.init({
    canvasConfig: { devicePixelRatio: 1 },
    // shadow setting
    setting: {
        shadow: {
            autoUpdate: true,
            shadowBound: 50,
            pointShadowBias: 0.0001,
            type: 'HARD'
        }
    }
});
let scene3D: Scene3D = new Scene3D();
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(60, engine.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(0, -45, 150, new Vector3(0, 0, 0));
scene3D.addChild(cameraObj);

//PointLight
{
    let obj = new Object3D();
    let light = obj.addComponent(PointLight);
    scene3D.addChild(obj);
    obj.x = -15;
    obj.y = 30;
    obj.z = -20;
    obj.rotationX = 0;
    obj.rotationY = 0;
    obj.rotationZ = 0;
    light.intensity = 15;
    light.range = 100;
    // enable light shadow
    light.castShadow = true;
    light.debug();
    light.debugDraw(true);
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(1, 0, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a plane to receive shadow
{
    let receiveShadowObj = new Object3D();
    let mr = receiveShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(2000, 1, 2000);
    mr.material = new LitMaterial();
    mr.receiveShadow = true;
    scene3D.addChild(receiveShadowObj);
}
// create a view with target scene and camera
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
// start render
engine.startRenderView(view);

Spot Light Shadow


WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, Camera3D, Vector3, AtmosphericComponent, View3D, LitMaterial, BoxGeometry, MeshRenderer, UnLitMaterial, SphereGeometry, DirectLight, PointLight, SpotLight, HoverCameraController, PlaneGeometry, Color } from '@orillusion/core';

let engine = await Engine3D.init({
    canvasConfig: { devicePixelRatio: 1 },
    // shadow setting
    setting: {
        shadow: {
            autoUpdate: true,
            shadowBound: 50,
            pointShadowBias: 0.0001,
            type: 'HARD'
        }
    }
});
let scene3D: Scene3D = new Scene3D();
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(60, engine.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(0, -45, 150, new Vector3(0, 0, 0));
scene3D.addChild(cameraObj);

//SpotLight
{
    let obj = new Object3D();
    let light = obj.addComponent(SpotLight);
    scene3D.addChild(obj);
    obj.x = -30;
    obj.y = 15;
    obj.z = 40;
    obj.rotationX = 0;
    obj.rotationY = 145;
    obj.rotationZ = 0;
    light.intensity = 50;
    light.range = 150;
    light.outerAngle = 110;
    light.innerAngle = 30;
    // enable light shadow
    light.castShadow = true;
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(1, 0, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a plane to receive shadow
{
    let receiveShadowObj = new Object3D();
    let mr = receiveShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(2000, 1, 2000);
    mr.material = new LitMaterial();
    mr.receiveShadow = true;
    scene3D.addChild(receiveShadowObj);
}
// create a view with target scene and camera
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
// start render
engine.startRenderView(view);

Shadow Bias

The shadow bias shadowBias is an important parameter that affects shadow rendering. Because the size of the shadow map and the size of the final rendering map are generally not exactly the same, it can cause situations such as shadow sampling distortion. Usually, you can solve the sampling distortion by manually setting a tiny bias shadowBias.

ts
light.shadowBias = 0.0002 // shadow bias

Version Note

Since 0.9.0, shadowBias is controlled on each type of light (previously on engine.setting.shadow). Its default value is auto, meaning the bias is calculated automatically; it can also be specified manually to override the auto value.

Generally, if shadowBias is set too small, large areas of moire or completely covered shadows will appear; conversely, if it is set too large, the shadow will separate from the object (light leakage):

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, Camera3D, Vector3, AtmosphericComponent, View3D, LitMaterial, BoxGeometry, MeshRenderer, UnLitMaterial, SphereGeometry, DirectLight, PointLight, SpotLight, HoverCameraController, PlaneGeometry, Color } from '@orillusion/core';
import * as dat from 'dat.gui';

let engine = await Engine3D.init({
    canvasConfig: { devicePixelRatio: 1 },
    setting: {
        // shadow setting
        shadow: {
            autoUpdate: true,
            debug: false,
            shadowBound: 100
        }
    }
});
let scene3D: Scene3D = new Scene3D();
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(60, engine.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(0, -45, 100, new Vector3(0, 0, 0));
scene3D.addChild(cameraObj);

{
    let obj = new Object3D();
    obj.y = 50;
    let light = obj.addComponent(DirectLight);
    scene3D.addChild(obj);
    obj.rotationX = 200;
    obj.rotationY = 135;
    obj.rotationZ = 170;
    light.intensity = 10;
    // enable light shadow
    light.castShadow = true;
    light.shadowBias = 0.01;
    const GUIHelp = new dat.GUI();
    GUIHelp.add(light, 'shadowBias', 0, 0.1, 0.0001);
}
// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(1, 0, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}
// create a plane to receive shadow
{
    let receiveShadowObj = new Object3D();
    let mr = receiveShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(2000, 1, 2000);
    mr.material = new LitMaterial();
    scene3D.addChild(receiveShadowObj);
}
// create a view with target scene and camera
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
// start render
engine.startRenderView(view);

Shadow Type

By setting the shadow type, you can control the visual effect of the shadow. The currently supported shadow types are:

  • HARD: Hard shadow, a relatively sharp shadow, usually used when the shadow is relatively close to the object.
  • SOFT: Soft shadow, a relatively soft, blurred shadow, with the shadow edges processed to be blurred, usually used when the shadow is relatively far from the object.
  • PCF: PCF (Percentage-Closer Filtering) is a common soft shadow processing algorithm. By sampling the current pixel and the surrounding shadow depths and performing a distance-weighted average, it obtains an artificially faked soft shadow effect. The engine currently uses this type of shadow by default.

Configuring the shadow type:

ts
engine.setting.shadow.type = 'SOFT'; // HARD by default

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Color } from '@orillusion/core';
import { Engine3D, Scene3D, Object3D, Camera3D, Vector3, AtmosphericComponent, View3D, LitMaterial, BoxGeometry, MeshRenderer, UnLitMaterial, SphereGeometry, DirectLight, PointLight, SpotLight, HoverCameraController, PlaneGeometry } from '@orillusion/core';
import * as dat from 'dat.gui';

let engine = await Engine3D.init({
    canvasConfig: { devicePixelRatio: 1 },
    // shadow setting
    setting: {
        shadow: {
            autoUpdate: true,
            shadowBound: 20,
            pointShadowBias: 0.0001,
            type: sessionStorage._shadow_type || 'HARD'
        }
    }
});
let scene3D: Scene3D = new Scene3D();
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(60, engine.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(0, -45, 50, new Vector3(0, 0, 0));
scene3D.addChild(cameraObj);

//PointLight
{
    let obj = new Object3D();
    let light = obj.addComponent(PointLight);
    scene3D.addChild(obj);
    obj.x = -30;
    obj.y = 30;
    obj.z = -30;
    obj.rotationX = 0;
    obj.rotationY = 0;
    obj.rotationZ = 0;
    light.intensity = 15;
    light.range = 100;
    light.castShadow = true;
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(1, 0, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a plane to receive shadow
{
    let receiveShadowObj = new Object3D();
    let mr = receiveShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(2000, 1, 2000);
    mr.material = new LitMaterial();
    mr.receiveShadow = true;
    scene3D.addChild(receiveShadowObj);
}
// create a view with target scene and camera
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
// start render
engine.startRenderView(view);

let gui = new dat.GUI();
gui.add(engine.setting.shadow, 'type', ['PCF', 'SOFT', 'HARD']).onChange((v) => {
    sessionStorage._shadow_type = v;
    location.reload();
});

Shadow Size

The engine's setting provides control parameters for the shadow size, which can be used to control the texture size and area size used by shadows in different scenes.

The relevant properties are as follows:

PropertyTypeDescription
shadowBoundNumberShadow area range
shadowSizeNumberDirect light shadow map size, default 1024
pointShadowSizeNumberPoint light shadow map size, default 1024

The shadow map size (shadowSize, pointShadowSize) directly affects the final shadow quality. The smaller the value, the lower the performance overhead, and the more obvious the shadow aliasing.

The shadowBound parameter controls the size of the illuminated shadow area in the scene. The larger the area, the more the shadow map size should be appropriately increased; when a large area is cast onto a small shadow map, it will also lead to very obvious shadow aliasing.

ts
engine.setting.shadow.shadowBound = 100

Set the direct light shadow map size:

ts
engine.setting.shadow.shadowSize = 2048

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, Camera3D, Vector3, AtmosphericComponent, View3D, LitMaterial, BoxGeometry, MeshRenderer, UnLitMaterial, SphereGeometry, DirectLight, PointLight, SpotLight, HoverCameraController, PlaneGeometry, Color } from '@orillusion/core';
import * as dat from 'dat.gui';

let engine = await Engine3D.init({
    canvasConfig: { devicePixelRatio: 1 },
    // shadow setting
    setting: {
        shadow: {
            autoUpdate: true,
            shadowBound: sessionStorage._shadowBound || 100,
            type: 'HARD',
            shadowSize: sessionStorage._shadowSize || 512
        }
    }
});
let scene3D: Scene3D = new Scene3D();
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(60, engine.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(-45, -45, 50, new Vector3(0, 0, 0));
scene3D.addChild(cameraObj);

//DirectLight
{
    let obj = new Object3D();
    obj.y = sessionStorage._shadowBound ? Number(sessionStorage._shadowBound) * 0.5 : 100;
    obj.rotationX = 45;
    obj.rotationY = 0;
    obj.rotationZ = 0;
    let light = obj.addComponent(DirectLight);
    scene3D.addChild(obj);
    // enable light shadow
    light.castShadow = true;
    light.shadowBias = 0.01;
    light.intensity = 10;
    scene3D.addChild(obj);
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    castShadowObj.rotationY = 45;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(1, 0, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    castShadowObj.rotationY = 45;
    castShadowObj.x = 30;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(0, 1, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a plane to receive shadow
{
    let receiveShadowObj = new Object3D();
    let mr = receiveShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(2000, 1, 2000);
    mr.material = new LitMaterial();
    mr.receiveShadow = true;
    scene3D.addChild(receiveShadowObj);
}
// create a view with target scene and camera
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
// start render
engine.startRenderView(view);

let gui = new dat.GUI();
gui.add(engine.setting.shadow, 'shadowBound', [30, 50, 100, 500]).onChange((v) => {
    sessionStorage._shadowBound = v;
    location.reload();
});
gui.add(engine.setting.shadow, 'shadowSize', [128, 256, 512, 1024, 2048]).onChange((v) => {
    sessionStorage._shadowSize = v;
    location.reload();
});

Set the point light shadow map size:

ts
engine.setting.shadow.pointShadowSize = 2048

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Engine3D, Scene3D, Object3D, Camera3D, Vector3, AtmosphericComponent, View3D, LitMaterial, BoxGeometry, MeshRenderer, UnLitMaterial, SphereGeometry, DirectLight, PointLight, SpotLight, HoverCameraController, PlaneGeometry, Color } from '@orillusion/core';
import * as dat from 'dat.gui';

let engine = await Engine3D.init({
    canvasConfig: { devicePixelRatio: 1 },
    // shadow setting
    setting: {
        shadow: {
            autoUpdate: true,
            shadowBound: sessionStorage._shadowBound || 100,
            type: 'HARD',
            pointShadowSize: sessionStorage._pointShadowSize || 512
        }
    }
});
let scene3D: Scene3D = new Scene3D();
let cameraObj: Object3D = new Object3D();
let camera = cameraObj.addComponent(Camera3D);
camera.perspective(60, engine.aspect, 1, 5000.0);
let controller = cameraObj.addComponent(HoverCameraController);
controller.setCamera(-45, -45, 50, new Vector3(0, 0, 0));
scene3D.addChild(cameraObj);

//PointLight
{
    let obj = new Object3D();
    obj.y = 50;
    obj.z = -30;
    obj.rotationX = 45;
    obj.rotationY = 0;
    obj.rotationZ = 0;
    let light = obj.addComponent(PointLight);
    scene3D.addChild(obj);
    // enable light shadow
    light.castShadow = true;
    light.intensity = 10;
    scene3D.addChild(obj);
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    castShadowObj.rotationY = 45;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(1, 0, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a box as shadow source
{
    let castShadowObj = new Object3D();
    castShadowObj.y = 5;
    castShadowObj.rotationY = 45;
    castShadowObj.x = 30;
    let mr = castShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(10, 10, 10);
    mr.material = new LitMaterial();
    mr.material.baseColor = new Color(0, 1, 0);
    mr.castShadow = true;
    scene3D.addChild(castShadowObj);
}

// create a plane to receive shadow
{
    let receiveShadowObj = new Object3D();
    let mr = receiveShadowObj.addComponent(MeshRenderer);
    mr.geometry = new BoxGeometry(2000, 1, 2000);
    mr.material = new LitMaterial();
    mr.receiveShadow = true;
    scene3D.addChild(receiveShadowObj);
}
// create a view with target scene and camera
let view = new View3D();
view.scene = scene3D;
view.camera = camera;
// start render
engine.startRenderView(view);

let gui = new dat.GUI();
gui.add(engine.setting.shadow, 'shadowBound', [20, 50, 100, 500]).onChange((v) => {
    sessionStorage._shadowBound = v;
    location.reload();
});
gui.add(engine.setting.shadow, 'pointShadowSize', [128, 256, 512, 1024, 2048]).onChange((v) => {
    sessionStorage._pointShadowSize = v;
    location.reload();
});

Cascaded Shadow Maps

Commonly used to support better shadow rendering effects in large scenes, it divides the shadows within the view frustum into four levels, and during the shading stage that processes shadows, selects the appropriate shadow map according to the current shadow level, obtaining a more accurate shadow effect that covers the full range of the view frustum. After using CSM shadows, it can solve the problem where the shadow Bound range is too large and the pixel density of the shadow is insufficient, causing excessively severe mosaicking; and where the Bound range is small and the shadow area is too small, causing distant objects to lose their shadows.

Starting from 0.9.0, CSM is controlled by the direct light (DirectLight) (previously on the camera). Just enable it on the direct light that casts the shadow:

ts
import { Object3D, DirectLight } from '@orillusion/core';

let lightObj = new Object3D();
let light = lightObj.addComponent(DirectLight);
light.castShadow = true;   // The direct light must first enable shadow casting
light.enableCSM = true;    // Enable cascaded shadows
light.cascadeNum = 4;      // Number of cascade levels (optional)

Migrating from Older Versions

In 0.8.x, CSM was enabled on the camera (camera.enableCSM = true); the camera's csm / getCSMShadowWorldExtents and other interfaces have been removed. Please set enableCSM on the DirectLight that casts the shadow instead. The csmMargin / csmScatteringExp / csmAreaScale that control the cascade range are still in engine.setting.shadow (see Shadow Properties below).

WebGPU is not supported in your browser
Please upgrade to latest Chrome/Edge

<
ts
import { Scene3D, HoverCameraController, Engine3D, AtmosphericComponent, Object3D, View3D, DirectLight, KelvinUtil, LitMaterial, MeshRenderer, BoxGeometry, CameraUtil, Object3DUtil } from '@orillusion/core';
import * as dat from 'dat.gui';

// sample of csm
class Sample_CSM {
    scene: Scene3D;
    view: View3D;
    light: DirectLight;
    GUIHelp: dat.GUI;
    engine: Engine3D;
    async run() {
        this.engine = await Engine3D.init({
            setting: {
                shadow: {
                    autoUpdate: true,
                    shadowSize: 2048,
                    shadowBound: 512,
                    shadowBias: 0.01
                }
            }
        });
        this.GUIHelp = new dat.GUI();

        this.scene = new Scene3D();
        let sky = this.scene.addComponent(AtmosphericComponent);

        // init camera3D
        let mainCamera = CameraUtil.createCamera3D(undefined, this.scene);
        mainCamera.perspective(60, this.engine.aspect, 1, 5000.0);
        mainCamera.object3D.z = -15;
        mainCamera.object3D.addComponent(HoverCameraController).setCamera(-15, -35, 200);

        // main directional light + CSM
        sky.relativeTransform = this.initLight('mainLight', 3, 45, true);
        this.initLight('subLight', 2, 10, false);
        this.initScene();

        let view = new View3D();
        view.scene = this.scene;
        view.camera = mainCamera;
        this.view = view;

        // CSM is controlled by the directional light (0.9.0+)
        let f = this.GUIHelp.addFolder('CSM');
        f.add(this.light, 'enableCSM');
        f.add(this.light, 'cascadeNum', 1, 4, 1);
        f.add(this.engine.setting.shadow, 'csmScatteringExp', 0.5, 1.0, 0.01);
        f.add(this.engine.setting.shadow, 'csmMargin', 0.01, 0.5, 0.01);
        f.add(this.engine.setting.shadow, 'csmAreaScale', 0.1, 1, 0.01);
        f.open();

        this.engine.startRenderView(view);
    }

    // create direction light
    private initLight(name: string, intensity: number, rotY: number, csm: boolean) {
        let lightObj3D = new Object3D();
        lightObj3D.name = name;
        lightObj3D.rotationX = 46;
        lightObj3D.rotationY = 62 + rotY;
        lightObj3D.rotationZ = 0;
        let sunLight = lightObj3D.addComponent(DirectLight);
        sunLight.intensity = intensity;
        sunLight.lightColor = KelvinUtil.color_temperature_to_rgb(6553);
        sunLight.castShadow = true;
        if (csm) {
            sunLight.enableCSM = true; // enable cascaded shadows on the directional light
            this.light = sunLight;
        }
        this.scene.addChild(lightObj3D);
        return sunLight.transform;
    }

    initScene() {
        // a tall pillar casting a long shadow spanning multiple cascades
        {
            let obj = new Object3D();
            let mr = obj.addComponent(MeshRenderer);
            mr.geometry = new BoxGeometry(20, 100, 20);
            mr.material = new LitMaterial();
            this.scene.addChild(obj);
        }

        // ground
        {
            let mat = new LitMaterial();
            mat.baseMap = this.engine.res.grayTexture;
            let floor = new Object3D();
            let mr = floor.addComponent(MeshRenderer);
            mr.geometry = new BoxGeometry(10000, 1, 10000);
            mr.material = mat;
            this.scene.addChild(floor);
        }

        // pillars spiraling outward into the distance to observe cascaded shadows at different ranges
        for (let i = 0; i < 1000; i++) {
            let item = Object3DUtil.GetSingleSphere(4, 0.6, 0.4, 0.2);
            let angle = (Math.PI * 4 * i) / 50;
            item.x = Math.sin(angle) * (50 + i ** 1.4);
            item.z = Math.cos(angle) * (50 + i ** 1.4);
            item.y = 4;
            let scale = (i ** 1.4 * 5 + 1000) / 1000;
            item.scaleX = item.scaleZ = scale;
            item.scaleY = scale * 5;
            this.scene.addChild(item);
        }
    }
}

new Sample_CSM().run();

Shadow Properties

PropertyTypeDescription
enableBooleanWhether to enable shadows, default false
typeStringShadow type, default PCF
shadowQualityNumberShadow rendering quality
shadowBoundNumberShadow area range
shadowSizeNumberDirect light shadow map size, default 1024; the smaller the value, the lower the performance overhead, but the more obvious the shadow aliasing
pointShadowSizeNumberPoint light shadow map size, default 1024
shadowBiasNumberShadow bias value of the light source
autoUpdateBooleanWhether to automatically update shadows, default false
csmMarginNumberSets the transition range between different levels of shadows, adjusted in the 0-1 range
csmScatteringExpNumberFine-tunes the range of each level of shadow to meet different scene needs
csmAreaScaleNumberFine-tunes the maximum range that shadows can cover, adjusted in the 0.0-1 range

Released under the MIT License