UNPKG

three

Version:

JavaScript 3D library

217 lines (124 loc) 5.65 kB
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 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(); 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;