Skip to content

Commit

Permalink
WebGPURenderer: RectAreaLight support (#28580)
Browse files Browse the repository at this point in the history
* WebGPURenderer: RectAreaLight support

* detect float32 texture filetring support

* cache runtime availbility checks

* remove testing code

* update screenshot

* excude from test

---------

Co-authored-by: aardgoose <[email protected]>
  • Loading branch information
aardgoose and aardgoose authored Jun 11, 2024
1 parent 276fa8e commit 2f33543
Show file tree
Hide file tree
Showing 11 changed files with 422 additions and 4 deletions.
1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@
"webgpu_lights_custom",
"webgpu_lights_ies_spotlight",
"webgpu_lights_phong",
"webgpu_lights_rectarealight",
"webgpu_lights_selective",
"webgpu_lines_fat",
"webgpu_loader_gltf",
Expand Down
1 change: 1 addition & 0 deletions examples/jsm/nodes/Nodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ export { default as ComputeNode, compute } from './gpgpu/ComputeNode.js';
export { default as LightNode, lightTargetDirection } from './lighting/LightNode.js';
export { default as PointLightNode } from './lighting/PointLightNode.js';
export { default as DirectionalLightNode } from './lighting/DirectionalLightNode.js';
export { default as RectAreaLightNode } from './lighting/RectAreaLightNode.js';
export { default as SpotLightNode } from './lighting/SpotLightNode.js';
export { default as IESSpotLightNode } from './lighting/IESSpotLightNode.js';
export { default as AmbientLightNode } from './lighting/AmbientLightNode.js';
Expand Down
2 changes: 2 additions & 0 deletions examples/jsm/nodes/core/LightingModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class LightingModel {

direct( /*input, stack, builder*/ ) { }

directRectArea( /*input, stack, builder*/ ) {}

indirectDiffuse( /*input, stack, builder*/ ) { }

indirectSpecular( /*input, stack, builder*/ ) { }
Expand Down
131 changes: 131 additions & 0 deletions examples/jsm/nodes/functions/BSDF/LTC.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { tslFn, If, mat3, vec2, vec3 } from '../../shadernode/ShaderNode.js';
import { max } from '../../math/MathNode.js';

// Rect Area Light

// Real-Time Polygonal-Light Shading with Linearly Transformed Cosines
// by Eric Heitz, Jonathan Dupuy, Stephen Hill and David Neubelt
// code: https://github.com/selfshadow/ltc_code/

const LTC_Uv = tslFn( ( { N, V, roughness } ) => {

const LUT_SIZE = 64.0;
const LUT_SCALE = ( LUT_SIZE - 1.0 ) / LUT_SIZE;
const LUT_BIAS = 0.5 / LUT_SIZE;

const dotNV = N.dot( V ).saturate();

// texture parameterized by sqrt( GGX alpha ) and sqrt( 1 - cos( theta ) )
const uv = vec2( roughness, dotNV.oneMinus().sqrt() );

uv.assign( uv.mul( LUT_SCALE ).add( LUT_BIAS ) );

return uv;

} ).setLayout( {
name: 'LTC_Uv',
type: 'vec2',
inputs: [
{ name: 'N', type: 'vec3' },
{ name: 'V', type: 'vec3' },
{ name: 'roughness', type: 'float' }
]
} );

const LTC_ClippedSphereFormFactor = tslFn( ( { f } ) => {

// Real-Time Area Lighting: a Journey from Research to Production (p.102)
// An approximation of the form factor of a horizon-clipped rectangle.

const l = f.length();

return max( l.mul( l ).add( f.z ).div( l.add( 1.0 ) ), 0 );

} ).setLayout( {
name: 'LTC_ClippedSphereFormFactor',
type: 'float',
inputs: [
{ name: 'f', type: 'vec3' }
]
} );

const LTC_EdgeVectorFormFactor = tslFn( ( { v1, v2 } ) => {

const x = v1.dot( v2 );
const y = x.abs().toVar();

// rational polynomial approximation to theta / sin( theta ) / 2PI
const a = y.mul( 0.0145206 ).add( 0.4965155 ).mul( y ).add( 0.8543985 ).toVar();
const b = y.add( 4.1616724 ).mul( y ).add( 3.4175940 ).toVar();
const v = a.div( b );

const theta_sintheta = x.greaterThan( 0.0 ).cond( v, max( x.mul( x ).oneMinus(), 1e-7 ).inverseSqrt().mul( 0.5 ).sub( v ) );

return v1.cross( v2 ).mul( theta_sintheta );

} ).setLayout( {
name: 'LTC_EdgeVectorFormFactor',
type: 'vec3',
inputs: [
{ name: 'v1', type: 'vec3' },
{ name: 'v2', type: 'vec3' }
]
} );

const LTC_Evaluate = tslFn( ( { N, V, P, mInv, p0, p1, p2, p3 } ) => {

// bail if point is on back side of plane of light
// assumes ccw winding order of light vertices
const v1 = p1.sub( p0 ).toVar();
const v2 = p3.sub( p0 ).toVar();

const lightNormal = v1.cross( v2 );
const result = vec3().toVar();

If( lightNormal.dot( P.sub( p0 ) ).greaterThanEqual( 0.0 ), () => {

// construct orthonormal basis around N
const T1 = V.sub( N.mul( V.dot( N ) ) ).normalize();
const T2 = N.cross( T1 ).negate(); // negated from paper; possibly due to a different handedness of world coordinate system

// compute transform
const mat = mInv.mul( mat3( T1, T2, N ).transpose() ).toVar();

// transform rect
// & project rect onto sphere
const coords0 = mat.mul( p0.sub( P ) ).normalize().toVar();
const coords1 = mat.mul( p1.sub( P ) ).normalize().toVar();
const coords2 = mat.mul( p2.sub( P ) ).normalize().toVar();
const coords3 = mat.mul( p3.sub( P ) ).normalize().toVar();

// calculate vector form factor
const vectorFormFactor = vec3( 0 ).toVar();
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords0, v2: coords1 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords1, v2: coords2 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords2, v2: coords3 } ) );
vectorFormFactor.addAssign( LTC_EdgeVectorFormFactor( { v1: coords3, v2: coords0 } ) );

// adjust for horizon clipping
result.assign( vec3( LTC_ClippedSphereFormFactor( { f: vectorFormFactor } ) ) );

} );

return result;

} ).setLayout( {
name: 'LTC_Evaluate',
type: 'vec3',
inputs: [
{ name: 'N', type: 'vec3' },
{ name: 'V', type: 'vec3' },
{ name: 'P', type: 'vec3' },
{ name: 'mInv', type: 'mat3' },
{ name: 'p0', type: 'vec3' },
{ name: 'p1', type: 'vec3' },
{ name: 'p2', type: 'vec3' },
{ name: 'p3', type: 'vec3' }
]
} );


export { LTC_Evaluate, LTC_Uv };
35 changes: 34 additions & 1 deletion examples/jsm/nodes/functions/PhysicalLightingModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ import EnvironmentBRDF from './BSDF/EnvironmentBRDF.js';
import F_Schlick from './BSDF/F_Schlick.js';
import Schlick_to_F0 from './BSDF/Schlick_to_F0.js';
import BRDF_Sheen from './BSDF/BRDF_Sheen.js';
import { LTC_Evaluate, LTC_Uv } from './BSDF/LTC.js';
import LightingModel from '../core/LightingModel.js';
import { diffuseColor, specularColor, specularF90, roughness, clearcoat, clearcoatRoughness, sheen, sheenRoughness, iridescence, iridescenceIOR, iridescenceThickness, ior, thickness, transmission, attenuationDistance, attenuationColor, dispersion } from '../core/PropertyNode.js';
import { transformedNormalView, transformedClearcoatNormalView, transformedNormalWorld } from '../accessors/NormalNode.js';
import { positionViewDirection, positionWorld } from '../accessors/PositionNode.js';
import { positionViewDirection, positionView, positionWorld } from '../accessors/PositionNode.js';
import { tslFn, float, vec2, vec3, vec4, mat3, If } from '../shadernode/ShaderNode.js';
import { cond } from '../math/CondNode.js';
import { mix, normalize, refract, length, clamp, log2, log, exp, smoothstep } from '../math/MathNode.js';
Expand Down Expand Up @@ -474,6 +475,38 @@ class PhysicalLightingModel extends LightingModel {

}

directRectArea( { lightColor, lightPosition, halfWidth, halfHeight, reflectedLight, ltc_1, ltc_2 } ) {

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 N = transformedNormalView;
const V = positionViewDirection;
const P = positionView.toVar();

const uv = LTC_Uv( { N, V, roughness } );

const t1 = ltc_1.uv( uv ).toVar();
const t2 = ltc_2.uv( uv ).toVar();

const mInv = mat3(
vec3( t1.x, 0, t1.y ),
vec3( 0, 1, 0 ),
vec3( t1.z, 0, t1.w )
).toVar();

// LTC Fresnel Approximation by Stephen Hill
// http://blog.selfshadow.com/publications/s2016-advances/s2016_ltc_fresnel.pdf
const fresnel = specularColor.mul( t2.x ).add( specularColor.oneMinus().mul( t2.y ) ).toVar();

reflectedLight.directSpecular.addAssign( lightColor.mul( fresnel ).mul( LTC_Evaluate( { N, V, P, mInv, p0, p1, p2, p3 } ) ) );

reflectedLight.directDiffuse.addAssign( lightColor.mul( diffuseColor ).mul( LTC_Evaluate( { N, V, P, mInv: mat3( 1, 0, 0, 0, 1, 0, 0, 0, 1 ), p0, p1, p2, p3 } ) ) );

}

indirectDiffuse( { irradiance, reflectedLight } ) {

reflectedLight.indirectDiffuse.addAssign( irradiance.mul( BRDF_Lambert( { diffuseColor } ) ) );
Expand Down
90 changes: 90 additions & 0 deletions examples/jsm/nodes/lighting/RectAreaLightNode.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import AnalyticLightNode from './AnalyticLightNode.js';
import { addLightNode } from './LightsNode.js';
import { texture } from '../accessors/TextureNode.js';
import { uniform } from '../core/UniformNode.js';
import { objectViewPosition } from '../accessors/Object3DNode.js';
import { addNodeClass } from '../core/Node.js';

import { RectAreaLight, Matrix4, Vector3, UniformsLib } from 'three';

const _matrix41 = new Matrix4();
const _matrix42 = new Matrix4();
let ltc_1, ltc_2;

class RectAreaLightNode extends AnalyticLightNode {

constructor( light = null ) {

super( light );

this.halfHeight = uniform( new Vector3() );
this.halfWidth = uniform( new Vector3() );

}

update( frame ) {

super.update( frame );

const { light } = this;

const viewMatrix = frame.camera.matrixWorldInverse;

_matrix42.identity();
_matrix41.copy( light.matrixWorld );
_matrix41.premultiply( viewMatrix );
_matrix42.extractRotation( _matrix41 );

this.halfWidth.value.set( light.width * 0.5, 0.0, 0.0 );
this.halfHeight.value.set( 0.0, light.height * 0.5, 0.0 );

this.halfWidth.value.applyMatrix4( _matrix42 );
this.halfHeight.value.applyMatrix4( _matrix42 );

}

setup( builder ) {

super.setup( builder );

if ( ltc_1 === undefined ) {

if ( builder.isAvailable( 'float32Filterable' ) ) {

ltc_1 = texture( UniformsLib.LTC_FLOAT_1 );
ltc_2 = texture( UniformsLib.LTC_FLOAT_2 );

} else {

ltc_1 = texture( UniformsLib.LTC_HALF_1 );
ltc_2 = texture( UniformsLib.LTC_HALF_2 );

}

}

const { colorNode, light } = this;
const lightingModel = builder.context.lightingModel;

const lightPosition = objectViewPosition( light );
const reflectedLight = builder.context.reflectedLight;

lightingModel.directRectArea( {
lightColor: colorNode,
lightPosition,
halfWidth: this.halfWidth,
halfHeight: this.halfHeight,
reflectedLight,
ltc_1,
ltc_2
}, builder.stack, builder );

}

}

export default RectAreaLightNode;

addNodeClass( 'RectAreaLightNode', RectAreaLightNode );

addLightNode( RectAreaLight, RectAreaLightNode );
30 changes: 28 additions & 2 deletions examples/jsm/renderers/webgl/nodes/GLSLNodeBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ const precisionLib = {
};

const supports = {
swizzleAssign: true
swizzleAssign: true,
storageBuffer: false
};

const defaultPrecisions = `
Expand Down Expand Up @@ -606,7 +607,32 @@ ${ flowData.code }

isAvailable( name ) {

return supports[ name ] === true;
let result = supports[ name ];

if ( result === undefined ) {

if ( name === 'float32Filterable' ) {

const extentions = this.renderer.backend.extensions;

if ( extentions.has( 'OES_texture_float_linear' ) ) {

extentions.get( 'OES_texture_float_linear' );
result = true;

} else {

result = false;

}

}

supports[ name ] = result;

}

return result;

}

Expand Down
17 changes: 16 additions & 1 deletion examples/jsm/renderers/webgpu/nodes/WGSLNodeBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const gpuShaderStageLib = {
};

const supports = {
swizzleAssign: false,
storageBuffer: true
};

Expand Down Expand Up @@ -1010,7 +1011,21 @@ ${ flowData.code }

isAvailable( name ) {

return supports[ name ] === true;
let result = supports[ name ];

if ( result === undefined ) {

if ( name === 'float32Filterable' ) {

result = this.renderer.hasFeature( 'float32-filterable' );

}

supports[ name ] = result;

}

return result;

}

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 2f33543

Please sign in to comment.