Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGPURenderer: RectAreaLight support #28580

Merged
merged 6 commits into from
Jun 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -550,7 +551,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