Skip to content

⬅️ Back to Table of Contents

📄 VolumetricLightingModel.js

📊 Analysis Summary

Metric Count
🔧 Functions 5
🧱 Classes 1
📦 Imports 17
📊 Variables & Constants 4

📚 Table of Contents

🛠️ File Location:

📂 src/nodes/functions/VolumetricLightingModel.js

📦 Imports

Name Source
LightingModel ../core/LightingModel.js
property ../core/PropertyNode.js
float ../tsl/TSLBase.js
If ../tsl/TSLBase.js
uniform ../tsl/TSLBase.js
vec3 ../tsl/TSLBase.js
vec4 ../tsl/TSLBase.js
positionWorld ../accessors/Position.js
cameraFar ../accessors/Camera.js
cameraNear ../accessors/Camera.js
cameraPosition ../accessors/Camera.js
cameraViewMatrix ../accessors/Camera.js
Loop ../utils/LoopNode.js
linearDepth ../display/ViewportDepthNode.js
viewZToPerspectiveDepth ../display/ViewportDepthNode.js
modelRadius ../accessors/ModelNode.js
LTC_Evaluate_Volume ./BSDF/LTC.js

Variables & Constants

Name Type Kind Value Exported
positionViewRay any let/var cameraViewMatrix.mul( vec4( positionRay, 1 ) ).xyz
scatteringNode any let/var *not shown*
sceneDepthNode any let/var builder.context.sceneDepthNode
P any let/var builder.context.positionView

Functions

VolumetricLightingModel.start(builder: any): void

Parameters:

  • builder any

Returns: void

Calls:

  • property (from ../core/PropertyNode.js)
  • `If( cameraPosition.sub( positionWorld ).length().greaterThan( modelRadius.mul( 2 ) ), () => {
        startPos.assign( cameraPosition );
        endPos.assign( positionWorld );
    
    } ).Else`
    
    • startPos.assign
    • endPos.assign
    • endPos.sub
    • uniform( 'int' ).onRenderUpdate
    • viewVector.length().div( steps ).toVar
    • viewVector.normalize().toVar
    • float( 0.0 ).toVar
    • vec3( 1 ).toVar
    • distTravelled.addAssign
    • material.offsetNode.mul
    • Loop (from ../utils/LoopNode.js)
    • startPos.add
    • rayDir.mul
    • cameraViewMatrix.mul
    • vec4 (from ../tsl/TSLBase.js)
    • linearDepthRay.assign
    • linearDepth (from ../display/ViewportDepthNode.js)
    • viewZToPerspectiveDepth (from ../display/ViewportDepthNode.js)
    • linearDepth( material.depthNode ).toVar
    • scatteringDensity.assign
    • material.scatteringNode
    • super.start
    • scatteringDensity.mulAssign
    • scatteringDensity.mul( .01 ).negate().mul( stepSize ).exp
    • transmittance.mulAssign
    • outgoingRayLight.addAssign
    • transmittance.saturate().oneMinus

Internal Comments:

// This approach dynamically changes the direction of the ray, (x5)
// prioritizing the ray from the camera to the object if it is inside the mesh, and from the object to the camera if it is far away. (x5)
// (x2)
// reduce banding (x4)
// beer's law (x2)
// move along the ray (x4)

Code
start( builder ) {

        const { material, context } = builder;

        const startPos = property( 'vec3' );
        const endPos = property( 'vec3' );

        // This approach dynamically changes the direction of the ray,
        // prioritizing the ray from the camera to the object if it is inside the mesh, and from the object to the camera if it is far away.

        If( cameraPosition.sub( positionWorld ).length().greaterThan( modelRadius.mul( 2 ) ), () => {

            startPos.assign( cameraPosition );
            endPos.assign( positionWorld );

        } ).Else( () => {

            startPos.assign( positionWorld );
            endPos.assign( cameraPosition );

        } );

        //

        const viewVector = endPos.sub( startPos );

        const steps = uniform( 'int' ).onRenderUpdate( ( { material } ) => material.steps );
        const stepSize = viewVector.length().div( steps ).toVar();

        const rayDir = viewVector.normalize().toVar(); // TODO: toVar() should be automatic here ( in loop )

        const distTravelled = float( 0.0 ).toVar();
        const transmittance = vec3( 1 ).toVar();

        if ( material.offsetNode ) {

            // reduce banding

            distTravelled.addAssign( material.offsetNode.mul( stepSize ) );

        }

        Loop( steps, () => {

            const positionRay = startPos.add( rayDir.mul( distTravelled ) );
            const positionViewRay = cameraViewMatrix.mul( vec4( positionRay, 1 ) ).xyz;

            if ( material.depthNode !== null ) {

                linearDepthRay.assign( linearDepth( viewZToPerspectiveDepth( positionViewRay.z, cameraNear, cameraFar ) ) );

                context.sceneDepthNode = linearDepth( material.depthNode ).toVar();

            }

            context.positionWorld = positionRay;
            context.shadowPositionWorld = positionRay;
            context.positionView = positionViewRay;

            scatteringDensity.assign( 0 );

            let scatteringNode;

            if ( material.scatteringNode ) {

                scatteringNode = material.scatteringNode( {
                    positionRay
                } );

            }

            super.start( builder );

            if ( scatteringNode ) {

                scatteringDensity.mulAssign( scatteringNode );

            }

            // beer's law

            const falloff = scatteringDensity.mul( .01 ).negate().mul( stepSize ).exp();
            transmittance.mulAssign( falloff );

            // move along the ray

            distTravelled.addAssign( stepSize );

        } );

        outgoingRayLight.addAssign( transmittance.saturate().oneMinus() );

    }

VolumetricLightingModel.scatteringLight(lightColor: any, builder: any): void

Parameters:

  • lightColor any
  • builder any

Returns: void

Calls:

  • If (from ../tsl/TSLBase.js)
  • sceneDepthNode.greaterThanEqual
  • scatteringDensity.addAssign
Code
scatteringLight( lightColor, builder ) {

        const sceneDepthNode = builder.context.sceneDepthNode;

        if ( sceneDepthNode ) {

            If( sceneDepthNode.greaterThanEqual( linearDepthRay ), () => {

                scatteringDensity.addAssign( lightColor );

            } );

        } else {

            scatteringDensity.addAssign( lightColor );

        }

    }

VolumetricLightingModel.direct({ lightNode, lightColor }: any, builder: any): void

Parameters:

  • { lightNode, lightColor } any
  • builder any

Returns: void

Calls:

  • lightColor.xyz.toVar
  • directLight.mulAssign
  • this.scatteringLight

Internal Comments:

// Ignore lights with infinite distance
// TODO: We need a viewportOpaque*() ( output, depth ) to fit with modern rendering approaches (x2)

Code
direct( { lightNode, lightColor }, builder ) {

        // Ignore lights with infinite distance

        if ( lightNode.light.distance === undefined ) return;

        // TODO: We need a viewportOpaque*() ( output, depth ) to fit with modern rendering approaches

        const directLight = lightColor.xyz.toVar();
        directLight.mulAssign( lightNode.shadowNode ); // it no should be necessary if used in the same render pass

        this.scatteringLight( directLight, builder );

    }

VolumetricLightingModel.directRectArea({ lightColor, lightPosition, halfWidth, halfHeight }: any, builder: any): void

Parameters:

  • { lightColor, lightPosition, halfWidth, halfHeight } any
  • builder any

Returns: void

Calls:

  • lightPosition.add( halfWidth ).sub
  • lightPosition.sub( halfWidth ).sub
  • lightPosition.sub( halfWidth ).add
  • lightPosition.add( halfWidth ).add
  • lightColor.xyz.mul( LTC_Evaluate_Volume( { P, p0, p1, p2, p3 } ) ).pow
  • this.scatteringLight
Code
directRectArea( { lightColor, lightPosition, halfWidth, halfHeight }, builder ) {

        const p0 = lightPosition.add( halfWidth ).sub( halfHeight ); // counterclockwise; light shines in local neg z direction
        const p1 = lightPosition.sub( halfWidth ).sub( halfHeight );
        const p2 = lightPosition.sub( halfWidth ).add( halfHeight );
        const p3 = lightPosition.add( halfWidth ).add( halfHeight );

        const P = builder.context.positionView;

        const directLight = lightColor.xyz.mul( LTC_Evaluate_Volume( { P, p0, p1, p2, p3 } ) ).pow( 1.5 );

        this.scatteringLight( directLight, builder );

    }

VolumetricLightingModel.finish(builder: any): void

Parameters:

  • builder any

Returns: void

Calls:

  • builder.context.outgoingLight.assign
Code
finish( builder ) {

        builder.context.outgoingLight.assign( outgoingRayLight );

    }

Classes

VolumetricLightingModel

Class Code
class VolumetricLightingModel extends LightingModel {

    constructor() {

        super();

    }

    start( builder ) {

        const { material, context } = builder;

        const startPos = property( 'vec3' );
        const endPos = property( 'vec3' );

        // This approach dynamically changes the direction of the ray,
        // prioritizing the ray from the camera to the object if it is inside the mesh, and from the object to the camera if it is far away.

        If( cameraPosition.sub( positionWorld ).length().greaterThan( modelRadius.mul( 2 ) ), () => {

            startPos.assign( cameraPosition );
            endPos.assign( positionWorld );

        } ).Else( () => {

            startPos.assign( positionWorld );
            endPos.assign( cameraPosition );

        } );

        //

        const viewVector = endPos.sub( startPos );

        const steps = uniform( 'int' ).onRenderUpdate( ( { material } ) => material.steps );
        const stepSize = viewVector.length().div( steps ).toVar();

        const rayDir = viewVector.normalize().toVar(); // TODO: toVar() should be automatic here ( in loop )

        const distTravelled = float( 0.0 ).toVar();
        const transmittance = vec3( 1 ).toVar();

        if ( material.offsetNode ) {

            // reduce banding

            distTravelled.addAssign( material.offsetNode.mul( stepSize ) );

        }

        Loop( steps, () => {

            const positionRay = startPos.add( rayDir.mul( distTravelled ) );
            const positionViewRay = cameraViewMatrix.mul( vec4( positionRay, 1 ) ).xyz;

            if ( material.depthNode !== null ) {

                linearDepthRay.assign( linearDepth( viewZToPerspectiveDepth( positionViewRay.z, cameraNear, cameraFar ) ) );

                context.sceneDepthNode = linearDepth( material.depthNode ).toVar();

            }

            context.positionWorld = positionRay;
            context.shadowPositionWorld = positionRay;
            context.positionView = positionViewRay;

            scatteringDensity.assign( 0 );

            let scatteringNode;

            if ( material.scatteringNode ) {

                scatteringNode = material.scatteringNode( {
                    positionRay
                } );

            }

            super.start( builder );

            if ( scatteringNode ) {

                scatteringDensity.mulAssign( scatteringNode );

            }

            // beer's law

            const falloff = scatteringDensity.mul( .01 ).negate().mul( stepSize ).exp();
            transmittance.mulAssign( falloff );

            // move along the ray

            distTravelled.addAssign( stepSize );

        } );

        outgoingRayLight.addAssign( transmittance.saturate().oneMinus() );

    }

    scatteringLight( lightColor, builder ) {

        const sceneDepthNode = builder.context.sceneDepthNode;

        if ( sceneDepthNode ) {

            If( sceneDepthNode.greaterThanEqual( linearDepthRay ), () => {

                scatteringDensity.addAssign( lightColor );

            } );

        } else {

            scatteringDensity.addAssign( lightColor );

        }

    }

    direct( { lightNode, lightColor }, builder ) {

        // Ignore lights with infinite distance

        if ( lightNode.light.distance === undefined ) return;

        // TODO: We need a viewportOpaque*() ( output, depth ) to fit with modern rendering approaches

        const directLight = lightColor.xyz.toVar();
        directLight.mulAssign( lightNode.shadowNode ); // it no should be necessary if used in the same render pass

        this.scatteringLight( directLight, builder );

    }

    directRectArea( { lightColor, lightPosition, halfWidth, halfHeight }, builder ) {

        const p0 = lightPosition.add( halfWidth ).sub( halfHeight ); // counterclockwise; light shines in local neg z direction
        const p1 = lightPosition.sub( halfWidth ).sub( halfHeight );
        const p2 = lightPosition.sub( halfWidth ).add( halfHeight );
        const p3 = lightPosition.add( halfWidth ).add( halfHeight );

        const P = builder.context.positionView;

        const directLight = lightColor.xyz.mul( LTC_Evaluate_Volume( { P, p0, p1, p2, p3 } ) ).pow( 1.5 );

        this.scatteringLight( directLight, builder );

    }

    finish( builder ) {

        builder.context.outgoingLight.assign( outgoingRayLight );

    }

}

Methods

start(builder: any): void
Code
start( builder ) {

        const { material, context } = builder;

        const startPos = property( 'vec3' );
        const endPos = property( 'vec3' );

        // This approach dynamically changes the direction of the ray,
        // prioritizing the ray from the camera to the object if it is inside the mesh, and from the object to the camera if it is far away.

        If( cameraPosition.sub( positionWorld ).length().greaterThan( modelRadius.mul( 2 ) ), () => {

            startPos.assign( cameraPosition );
            endPos.assign( positionWorld );

        } ).Else( () => {

            startPos.assign( positionWorld );
            endPos.assign( cameraPosition );

        } );

        //

        const viewVector = endPos.sub( startPos );

        const steps = uniform( 'int' ).onRenderUpdate( ( { material } ) => material.steps );
        const stepSize = viewVector.length().div( steps ).toVar();

        const rayDir = viewVector.normalize().toVar(); // TODO: toVar() should be automatic here ( in loop )

        const distTravelled = float( 0.0 ).toVar();
        const transmittance = vec3( 1 ).toVar();

        if ( material.offsetNode ) {

            // reduce banding

            distTravelled.addAssign( material.offsetNode.mul( stepSize ) );

        }

        Loop( steps, () => {

            const positionRay = startPos.add( rayDir.mul( distTravelled ) );
            const positionViewRay = cameraViewMatrix.mul( vec4( positionRay, 1 ) ).xyz;

            if ( material.depthNode !== null ) {

                linearDepthRay.assign( linearDepth( viewZToPerspectiveDepth( positionViewRay.z, cameraNear, cameraFar ) ) );

                context.sceneDepthNode = linearDepth( material.depthNode ).toVar();

            }

            context.positionWorld = positionRay;
            context.shadowPositionWorld = positionRay;
            context.positionView = positionViewRay;

            scatteringDensity.assign( 0 );

            let scatteringNode;

            if ( material.scatteringNode ) {

                scatteringNode = material.scatteringNode( {
                    positionRay
                } );

            }

            super.start( builder );

            if ( scatteringNode ) {

                scatteringDensity.mulAssign( scatteringNode );

            }

            // beer's law

            const falloff = scatteringDensity.mul( .01 ).negate().mul( stepSize ).exp();
            transmittance.mulAssign( falloff );

            // move along the ray

            distTravelled.addAssign( stepSize );

        } );

        outgoingRayLight.addAssign( transmittance.saturate().oneMinus() );

    }
scatteringLight(lightColor: any, builder: any): void
Code
scatteringLight( lightColor, builder ) {

        const sceneDepthNode = builder.context.sceneDepthNode;

        if ( sceneDepthNode ) {

            If( sceneDepthNode.greaterThanEqual( linearDepthRay ), () => {

                scatteringDensity.addAssign( lightColor );

            } );

        } else {

            scatteringDensity.addAssign( lightColor );

        }

    }
direct({ lightNode, lightColor }: any, builder: any): void
Code
direct( { lightNode, lightColor }, builder ) {

        // Ignore lights with infinite distance

        if ( lightNode.light.distance === undefined ) return;

        // TODO: We need a viewportOpaque*() ( output, depth ) to fit with modern rendering approaches

        const directLight = lightColor.xyz.toVar();
        directLight.mulAssign( lightNode.shadowNode ); // it no should be necessary if used in the same render pass

        this.scatteringLight( directLight, builder );

    }
directRectArea({ lightColor, lightPosition, halfWidth, halfHeight }: any, builder: any): void
Code
directRectArea( { lightColor, lightPosition, halfWidth, halfHeight }, builder ) {

        const p0 = lightPosition.add( halfWidth ).sub( halfHeight ); // counterclockwise; light shines in local neg z direction
        const p1 = lightPosition.sub( halfWidth ).sub( halfHeight );
        const p2 = lightPosition.sub( halfWidth ).add( halfHeight );
        const p3 = lightPosition.add( halfWidth ).add( halfHeight );

        const P = builder.context.positionView;

        const directLight = lightColor.xyz.mul( LTC_Evaluate_Volume( { P, p0, p1, p2, p3 } ) ).pow( 1.5 );

        this.scatteringLight( directLight, builder );

    }
finish(builder: any): void
Code
finish( builder ) {

        builder.context.outgoingLight.assign( outgoingRayLight );

    }