Accueil / npm / three@0.186.1 / src / nodes / functions / VolumetricLightingModel.js

VolumetricLightingModel.js

import LightingModel from '../core/LightingModel.js';
import { property } from '../core/PropertyNode.js';
import { float, If, uniform, vec3, vec4 } from '../tsl/TSLBase.js';
import { positionWorld } from '../accessors/Position.js';
import { cameraFar, cameraNear, cameraPosition, cameraViewMatrix } from '../accessors/Camera.js';
import { Loop } from '../utils/LoopNode.js';
import { linearDepth, viewZToPerspectiveDepth } from '../display/ViewportDepthNode.js';
import { modelRadius } from '../accessors/ModelNode.js';
import { LTC_Evaluate_Volume } from './BSDF/LTC.js';

const scatteringDensity = property( 'vec3' );
const linearDepthRay = property( 'vec3' );
const outgoingRayLight = property( 'vec3' );

/**
 * VolumetricLightingModel class extends the LightingModel to implement volumetric lighting effects.
 * This model calculates the scattering and transmittance of light through a volumetric medium.
 * It dynamically adjusts the direction of the ray based on the camera and object positions.
 * The model supports custom scattering and depth nodes to enhance the lighting effects.
 *
 * @augments LightingModel
 */
class VolumetricLightingModel extends LightingModel {

	constructor() {

		super();

	}

	start( builder ) {

		const { material } = 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.

		const isFrontToBack = property( 'bool' );

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

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

		} ).Else( () => {

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

		} );

		//

		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 ) ) );

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

			}

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

			scatteringDensity.assign( 0 );

			let scatteringNode;
			let scatteringEmissiveNode;

			if ( material.scatteringNode ) {

				scatteringNode = material.scatteringNode( { positionRay } );

			}

			if ( material.scatteringEmissiveNode ) {

				scatteringEmissiveNode = material.scatteringEmissiveNode( { positionRay } );

			}

			super.start( builder );

			if ( scatteringNode ) {

				scatteringDensity.mulAssign( scatteringNode );

			}

			const stepLight = scatteringDensity.mul( 0.01 ).toVar();

			if ( scatteringEmissiveNode ) {

				stepLight.addAssign( scatteringEmissiveNode.mul( 0.01 ) );

			}

			// beer's law

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

			If( isFrontToBack, () => {

				outgoingRayLight.addAssign( stepLight.mul( transmittance ).mul( stepSize ) );

			} ).Else( () => {

				outgoingRayLight.assign( outgoingRayLight.mul( falloff ).add( stepLight.mul( stepSize ) ) );

			} );

			transmittance.mulAssign( falloff );

			// move along the ray

			distTravelled.addAssign( stepSize );

		} );



	}

	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 non-analytical lights and lights with infinite distance

		if ( lightNode.isAnalyticLightNode !== true || lightNode.light.distance === undefined ) return;

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

		const directLight = lightColor.xyz.toVar();

		if ( lightNode.shadowNode !== null ) {

			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 );

	}

}

export default VolumetricLightingModel;