three
Version:
JavaScript 3D library
184 lines (109 loc) • 4.97 kB
JavaScript
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, 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 );
}
}
export default VolumetricLightingModel;