Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build-module
run: npm run build

- name: === E2E testing ===
run: xvfb-run -a npm run test-e2e
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/read-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ jobs:
- name: Read bundle sizes
id: read-size
run: |
# minify the builds to measure their size
npx terser build/three.module.js --module --compress --mangle --output build/three.module.min.js
npx terser build/three.webgpu.js --module --compress --mangle --output build/three.webgpu.min.js
npx terser build/three.webgpu.nodes.js --module --compress --mangle --output build/three.webgpu.nodes.min.js

WEBGL_FILESIZE=$(stat --format=%s build/three.module.min.js)
gzip -k build/three.module.min.js
WEBGL_FILESIZE_GZIP=$(stat --format=%s build/three.module.min.js.gz)
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/report-size.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ jobs:
- name: Read sizes
id: read-size
run: |
# minify the builds to measure their size
npx terser build/three.module.js --module --compress --mangle --output build/three.module.min.js
npx terser build/three.webgpu.js --module --compress --mangle --output build/three.webgpu.min.js
npx terser build/three.webgpu.nodes.js --module --compress --mangle --output build/three.webgpu.nodes.min.js

WEBGL_FILESIZE_BASE=$(stat --format=%s build/three.module.min.js)
gzip -k build/three.module.min.js
WEBGL_FILESIZE_BASE_GZIP=$(stat --format=%s build/three.module.min.js.gz)
Expand Down
6 changes: 0 additions & 6 deletions build/three.core.min.js

This file was deleted.

6 changes: 0 additions & 6 deletions build/three.module.min.js

This file was deleted.

6 changes: 0 additions & 6 deletions build/three.tsl.min.js

This file was deleted.

6 changes: 0 additions & 6 deletions build/three.webgpu.min.js

This file was deleted.

6 changes: 0 additions & 6 deletions build/three.webgpu.nodes.min.js

This file was deleted.

1 change: 1 addition & 0 deletions examples/files.json
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,7 @@
"webgpu_ocean",
"webgpu_parallax_uv",
"webgpu_particles",
"webgpu_particles_soft",
"webgpu_performance",
"webgpu_performance_renderbundle",
"webgpu_pmrem_cubemap",
Expand Down
64 changes: 50 additions & 14 deletions examples/jsm/tsl/display/GTAONode.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { DataTexture, RenderTarget, RepeatWrapping, Vector2, Vector3, TempNode, QuadMesh, NodeMaterial, RendererUtils, RedFormat } from 'three/webgpu';
import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, getScreenPosition, getViewPosition, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, vec4, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, mul, cross, div, mix, acos, clamp } from 'three/tsl';
import { reference, logarithmicDepthToViewZ, viewZToPerspectiveDepth, getNormalFromDepth, getScreenPosition, getViewPosition, nodeObject, Fn, float, NodeUpdateType, uv, uniform, Loop, vec2, vec3, int, dot, max, pow, abs, If, textureSize, sin, cos, PI, texture, passTexture, mat3, add, normalize, cross, mix, acos, clamp, interleavedGradientNoise, screenCoordinate, fract, rand } from 'three/tsl';

const _quadMesh = /*@__PURE__*/ new QuadMesh();
const _size = /*@__PURE__*/ new Vector2();

// From Activision GTAO paper: https://www.activision.com/cdn/research/s2016_pbs_activision_occlusion.pptx
const _temporalRotations = [ 60, 300, 180, 240, 120, 0 ];
const _spatialOffsets = [ 0, 0.5, 0.25, 0.75 ];

let _rendererState;

Expand Down Expand Up @@ -122,17 +123,18 @@ class GTAONode extends TempNode {
this.thickness = uniform( 1 );

/**
* Another option to tweak the occlusion. The recommended range is
* `[1,2]` for attenuating the AO.
* @deprecated Since the switch to quadratic ray stepping with sphere falloff,
* step distribution is fixed at `t²` and this uniform has no effect. Kept for
* backward compatibility and will be removed in a future release.
*
* @type {UniformNode<float>}
*/
this.distanceExponent = uniform( 1 );

/**
* The distance fall off value of the ambient occlusion.
* A lower value leads to a larger AO effect. The value
* should lie in the range `[0,1]`.
* @deprecated Replaced by the sphere falloff `mix( max( h, sH ), h, (dist/radius)² )`,
* which has no tunable parameter. Kept for backward compatibility and will be
* removed in a future release.
*
* @type {UniformNode<float>}
*/
Expand Down Expand Up @@ -216,6 +218,15 @@ class GTAONode extends TempNode {
*/
this._temporalDirection = uniform( 0 );

/**
* Temporal offset added to the initial ray step.
*
* @private
* @type {UniformNode<float>}
*/
this._temporalOffset = uniform( 0 );


/**
* The material that is used to render the effect.
*
Expand Down Expand Up @@ -280,10 +291,12 @@ class GTAONode extends TempNode {
const frameId = frame.frameId;

this._temporalDirection.value = _temporalRotations[ frameId % 6 ] / 360;
this._temporalOffset.value = _spatialOffsets[ frameId % 4 ];

} else {

this._temporalDirection.value = 0;
this._temporalOffset.value = 1;

}

Expand Down Expand Up @@ -367,14 +380,18 @@ class GTAONode extends TempNode {

// Each iteration analyzes one vertical "slice" of the 3D space around the fragment.

// Per-step phase jitter for spatio-temporal decorrelation.
const noiseJitterIdx = this._temporalDirection.mul( 0.02 );
const stepJitter = fract( interleavedGradientNoise( screenCoordinate.add( this._temporalOffset ) ) ).add( rand( uvNode.add( noiseJitterIdx ).mul( 2 ).sub( 1 ) ) );

Loop( { start: int( 0 ), end: DIRECTIONS, type: 'int', condition: '<' }, ( { i } ) => {

const angle = float( i ).div( float( DIRECTIONS ) ).mul( PI ).add( this._temporalDirection ).toVar();
const sampleDir = vec4( cos( angle ), sin( angle ), 0., add( 0.5, mul( 0.5, noiseTexel.w ) ) );
sampleDir.xyz = normalize( kernelMatrix.mul( sampleDir.xyz ) );
const sampleDir = vec3( cos( angle ), sin( angle ), 0 ).toVar();
sampleDir.assign( normalize( kernelMatrix.mul( sampleDir ) ) );

const viewDir = normalize( viewPosition.xyz.negate() ).toVar();
const sliceBitangent = normalize( cross( sampleDir.xyz, viewDir ) ).toVar();
const sliceBitangent = normalize( cross( sampleDir, viewDir ) ).toVar();
const sliceTangent = cross( sliceBitangent, viewDir ).toVar();

// Project the view normal onto the slice plane (remove component along sliceBitangent).
Expand All @@ -397,7 +414,11 @@ class GTAONode extends TempNode {

Loop( { end: STEPS, type: 'int', name: 'j', condition: '<' }, ( { j } ) => {

const sampleViewOffset = sampleDir.xyz.mul( radiusToUse ).mul( sampleDir.w ).mul( pow( div( float( j ).add( 1.0 ), float( STEPS ) ), this.distanceExponent ) );
// Quadratic step distribution ( sampleDist = t² ) concentrates samples in the
// near-field. (Blender's Eevee adaptation)
const t = float( j ).add( 1.0 ).add( stepJitter ).div( STEPS ).toVar();
const sampleDist = t.mul( t );
const sampleViewOffset = sampleDir.mul( radiusToUse ).mul( sampleDist );

// The loop marches in two opposite directions (x and y) along the slice's line to find the horizon on both sides.

Expand All @@ -407,11 +428,21 @@ class GTAONode extends TempNode {
const sampleDepthX = sampleDepth( sampleScreenPositionX ).toVar();
const sampleSceneViewPositionX = getViewPosition( sampleScreenPositionX, sampleDepthX, this._cameraProjectionMatrixInverse ).toVar();
const viewDeltaX = sampleSceneViewPositionX.sub( viewPosition ).toVar();
const lenX = viewDeltaX.length().toVar();

// Manual normalize guards against zero-length delta.
const sHX = dot( viewDir, viewDeltaX.div( max( lenX, float( 0.0001 ) ) ) );

// Sphere falloff: ( dist / radius )² fades the sample's horizon contribution
// back toward the prior horizon as it approaches the radius boundary.
// (squared variant of the paper's near-field attenuation;
// Activision GTAO paper, Section 4.3 "Bounding the sampling area")
const distFacX = clamp( lenX.div( radiusToUse ), 0, 1 );
const distFacSqX = distFacX.mul( distFacX );

If( abs( viewDeltaX.z ).lessThan( this.thickness ), () => {

const sampleCosHorizon = dot( viewDir, normalize( viewDeltaX ) );
cosHorizons.x.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.x ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) );
cosHorizons.x.assign( mix( max( cosHorizons.x, sHX ), cosHorizons.x, distFacSqX ) );

} );

Expand All @@ -421,11 +452,16 @@ class GTAONode extends TempNode {
const sampleDepthY = sampleDepth( sampleScreenPositionY ).toVar();
const sampleSceneViewPositionY = getViewPosition( sampleScreenPositionY, sampleDepthY, this._cameraProjectionMatrixInverse ).toVar();
const viewDeltaY = sampleSceneViewPositionY.sub( viewPosition ).toVar();
const lenY = viewDeltaY.length().toVar();

const sHY = dot( viewDir, viewDeltaY.div( max( lenY, float( 0.0001 ) ) ) );

const distFacY = clamp( lenY.div( radiusToUse ), 0, 1 );
const distFacSqY = distFacY.mul( distFacY );

If( abs( viewDeltaY.z ).lessThan( this.thickness ), () => {

const sampleCosHorizon = dot( viewDir, normalize( viewDeltaY ) );
cosHorizons.y.addAssign( max( 0, mul( sampleCosHorizon.sub( cosHorizons.y ), mix( 1.0, float( 2.0 ).div( float( j ).add( 2 ) ), this.distanceFallOff ) ) ) );
cosHorizons.y.assign( mix( max( cosHorizons.y, sHY ), cosHorizons.y, distFacSqY ) );

} );

Expand Down
60 changes: 60 additions & 0 deletions examples/jsm/tsl/utils/SoftParticles.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Fn, float, positionView, viewportDepthTexture, perspectiveDepthToViewZ, cameraNear, cameraFar } from 'three/tsl';

/**
* @module SoftParticles
* @three_import import { softParticles } from 'three/addons/tsl/utils/SoftParticles.js';
*/

/**
* A symmetric contrast curve, as described in the "Soft Particles" white paper
* (NVIDIA, Tristan Lorach).
*
* @tsl
* @function
* @private
* @param {Node<float>} input - The value to remap, expected in the `[0, 1]` range.
* @param {Node<float>} power - The contrast power. `1` is linear, higher values sharpen the transition.
* @return {Node<float>} The remapped value.
*/
const contrastCurve = /*@__PURE__*/ Fn( ( [ input, power ] ) => {

const aboveHalf = input.greaterThan( 0.5 ).toConst();
const folded = aboveHalf.select( input.oneMinus(), input ).toConst();
const output = folded.mul( 2 ).saturate().pow( power ).mul( 0.5 ).toConst();

return aboveHalf.select( output.oneMinus(), output );

} ).setLayout( {
name: 'contrastCurve',
type: 'float',
inputs: [
{ name: 'input', type: 'float' },
{ name: 'power', type: 'float' }
]
} );

/**
* Computes an opacity node for soft particles, based on the "Soft Particles" white
* paper (NVIDIA, Tristan Lorach).
*
* @tsl
* @function
* @param {Object} [parameters={}] - The configuration parameters.
* @param {Node<float>} [parameters.opacity=float(1)] - The sprite's base opacity, which the soft fade is multiplied with.
* @param {Node<float>|number} [parameters.distance=1] - The world-space distance over which the sprite fades out against the scene.
* @param {Node<float>|number} [parameters.contrast=2] - The contrast power of the fade curve. `1` is linear, higher values sharpen the transition.
* @param {Node<float>} [parameters.viewportDepth=viewportDepthTexture()] - The opaque scene depth the particles fade against.
* @return {Node<float>} The opacity node to assign to `material.opacityNode`.
*/
export function softParticles( { opacity = float( 1 ), distance = 1, contrast = 2, viewportDepth = viewportDepthTexture() } = {} ) {

// Read the opaque scene depth captured before the particle pass and convert both
// the scene depth and the particle's depth to view space, so the gap between them
// can be measured in world units.
const sceneViewZ = perspectiveDepthToViewZ( viewportDepth, cameraNear, cameraFar ).toConst();
const depthDelta = positionView.z.sub( sceneViewZ ).div( distance ).saturate();

// Fade out as the particle approaches the scene; stay opaque when well in front of it.
return opacity.mul( contrastCurve( depthDelta, contrast ) );

}
Binary file added examples/screenshots/webgpu_particles_soft.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/screenshots/webgpu_postprocessing_ao.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading