Skip to content

Commit ff6450a

Browse files
authored
NodeMaterial: Add support for alphaHash. (#29757)
* NodeMaterial: Add support for `alphaHash`. * Clean up. * E2E: Update exception list. * Examples: Clean up. * NodeMaterial: Fix alpha hash check.
1 parent bbfdeac commit ff6450a

File tree

6 files changed

+247
-0
lines changed

6 files changed

+247
-0
lines changed

examples/files.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@
350350
"webgpu_loader_gltf_transmission",
351351
"webgpu_loader_materialx",
352352
"webgpu_materials",
353+
"webgpu_materials_alphahash",
353354
"webgpu_materials_arrays",
354355
"webgpu_materials_basic",
355356
"webgpu_materials_displacementmap",
29.2 KB
Loading
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<title>three.js webgpu - materials - alpha hash</title>
5+
<meta charset="utf-8">
6+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
7+
<link type="text/css" rel="stylesheet" href="main.css">
8+
</head>
9+
<body>
10+
<script type="importmap">
11+
{
12+
"imports": {
13+
"three": "../build/three.webgpu.js",
14+
"three/tsl": "../build/three.webgpu.js",
15+
"three/addons/": "./jsm/"
16+
}
17+
}
18+
</script>
19+
20+
<script type="module">
21+
22+
import * as THREE from 'three';
23+
24+
import Stats from 'three/addons/libs/stats.module.js';
25+
import { GUI } from 'three/addons/libs/lil-gui.module.min.js';
26+
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
27+
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
28+
29+
import { ssaaPass } from 'three/addons/tsl/display/SSAAPassNode.js';
30+
31+
let camera, scene, renderer, controls, stats, mesh, material, postProcessing;
32+
33+
const amount = parseInt( window.location.search.slice( 1 ) ) || 3;
34+
const count = Math.pow( amount, 3 );
35+
36+
const color = new THREE.Color();
37+
38+
const params = {
39+
alpha: 0.5,
40+
alphaHash: true
41+
};
42+
43+
init();
44+
45+
async function init() {
46+
47+
camera = new THREE.PerspectiveCamera( 60, window.innerWidth / window.innerHeight, 0.1, 100 );
48+
camera.position.set( amount, amount, amount );
49+
camera.lookAt( 0, 0, 0 );
50+
51+
scene = new THREE.Scene();
52+
53+
const geometry = new THREE.IcosahedronGeometry( 0.5, 3 );
54+
55+
material = new THREE.MeshStandardMaterial( {
56+
color: 0xffffff,
57+
alphaHash: params.alphaHash,
58+
opacity: params.alpha
59+
} );
60+
61+
mesh = new THREE.InstancedMesh( geometry, material, count );
62+
63+
let i = 0;
64+
const offset = ( amount - 1 ) / 2;
65+
66+
const matrix = new THREE.Matrix4();
67+
68+
for ( let x = 0; x < amount; x ++ ) {
69+
70+
for ( let y = 0; y < amount; y ++ ) {
71+
72+
for ( let z = 0; z < amount; z ++ ) {
73+
74+
matrix.setPosition( offset - x, offset - y, offset - z );
75+
76+
mesh.setMatrixAt( i, matrix );
77+
mesh.setColorAt( i, color.setHex( Math.random() * 0xffffff ) );
78+
79+
i ++;
80+
81+
}
82+
83+
}
84+
85+
}
86+
87+
scene.add( mesh );
88+
89+
//
90+
91+
renderer = new THREE.WebGPURenderer();
92+
renderer.setPixelRatio( window.devicePixelRatio );
93+
renderer.setSize( window.innerWidth, window.innerHeight );
94+
renderer.setAnimationLoop( animate );
95+
document.body.appendChild( renderer.domElement );
96+
97+
await renderer.init();
98+
99+
//
100+
101+
const environment = new RoomEnvironment();
102+
const pmremGenerator = new THREE.PMREMGenerator( renderer );
103+
104+
scene.environment = pmremGenerator.fromScene( environment ).texture;
105+
environment.dispose();
106+
107+
//
108+
109+
// postprocessing
110+
111+
postProcessing = new THREE.PostProcessing( renderer );
112+
const scenePass = ssaaPass( scene, camera );
113+
scenePass.sampleLevel = 3;
114+
115+
postProcessing.outputNode = scenePass;
116+
117+
//
118+
119+
controls = new OrbitControls( camera, renderer.domElement );
120+
controls.enableZoom = false;
121+
controls.enablePan = false;
122+
123+
//
124+
125+
const gui = new GUI();
126+
127+
gui.add( params, 'alpha', 0, 1 ).onChange( onMaterialUpdate );
128+
gui.add( params, 'alphaHash' ).onChange( onMaterialUpdate );
129+
130+
const ssaaFolder = gui.addFolder( 'SSAA' );
131+
ssaaFolder.add( scenePass, 'sampleLevel', 0, 4, 1 );
132+
133+
//
134+
135+
stats = new Stats();
136+
document.body.appendChild( stats.dom );
137+
138+
window.addEventListener( 'resize', onWindowResize );
139+
140+
}
141+
142+
function onWindowResize() {
143+
144+
camera.aspect = window.innerWidth / window.innerHeight;
145+
camera.updateProjectionMatrix();
146+
147+
renderer.setSize( window.innerWidth, window.innerHeight );
148+
149+
}
150+
151+
function onMaterialUpdate() {
152+
153+
material.opacity = params.alpha;
154+
material.alphaHash = params.alphaHash;
155+
material.transparent = ! params.alphaHash;
156+
material.depthWrite = params.alphaHash;
157+
158+
material.needsUpdate = true;
159+
160+
}
161+
162+
function animate() {
163+
164+
postProcessing.render();
165+
166+
stats.update();
167+
168+
}
169+
170+
</script>
171+
</body>
172+
</html>

src/materials/nodes/NodeMaterial.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { depth, perspectiveDepthToLogarithmicDepth, viewZToOrthographicDepth } f
2222
import { cameraFar, cameraNear } from '../../nodes/accessors/Camera.js';
2323
import { clipping, clippingAlpha } from '../../nodes/accessors/ClippingNode.js';
2424
import NodeMaterialObserver from './manager/NodeMaterialObserver.js';
25+
import getAlphaHashThreshold from '../../nodes/functions/material/getAlphaHashThreshold.js';
2526

2627
class NodeMaterial extends Material {
2728

@@ -367,6 +368,14 @@ class NodeMaterial extends Material {
367368

368369
}
369370

371+
// ALPHA HASH
372+
373+
if ( this.alphaHash === true ) {
374+
375+
diffuseColor.a.lessThan( getAlphaHashThreshold( positionLocal ) ).discard();
376+
377+
}
378+
370379
if ( this.transparent === false && this.blending === NormalBlending && this.alphaToCoverage === false ) {
371380

372381
diffuseColor.a.assign( 1.0 );
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { abs, add, ceil, clamp, dFdx, dFdy, exp2, float, floor, Fn, fract, length, log2, max, min, mul, sin, sub, vec2, vec3 } from '../../tsl/TSLBase.js';
2+
3+
/**
4+
* See: https://casual-effects.com/research/Wyman2017Hashed/index.html
5+
*/
6+
7+
const ALPHA_HASH_SCALE = 0.05; // Derived from trials only, and may be changed.
8+
9+
const hash2D = /*@__PURE__*/ Fn( ( [ value ] ) => {
10+
11+
return fract( mul( 1.0e4, sin( mul( 17.0, value.x ).add( mul( 0.1, value.y ) ) ) ).mul( add( 0.1, abs( sin( mul( 13.0, value.y ).add( value.x ) ) ) ) ) );
12+
13+
} );
14+
15+
const hash3D = /*@__PURE__*/ Fn( ( [ value ] ) => {
16+
17+
return hash2D( vec2( hash2D( value.xy ), value.z ) );
18+
19+
} );
20+
21+
const getAlphaHashThreshold = /*@__PURE__*/ Fn( ( [ position ] ) => {
22+
23+
// Find the discretized derivatives of our coordinates
24+
const maxDeriv = max(
25+
length( dFdx( position.xyz ) ),
26+
length( dFdy( position.xyz ) )
27+
).toVar( 'maxDeriv' );
28+
29+
const pixScale = float( 1 ).div( float( ALPHA_HASH_SCALE ).mul( maxDeriv ) ).toVar( 'pixScale' );
30+
31+
// Find two nearest log-discretized noise scales
32+
const pixScales = vec2(
33+
exp2( floor( log2( pixScale ) ) ),
34+
exp2( ceil( log2( pixScale ) ) )
35+
).toVar( 'pixScales' );
36+
37+
// Compute alpha thresholds at our two noise scales
38+
const alpha = vec2(
39+
hash3D( floor( pixScales.x.mul( position.xyz ) ) ),
40+
hash3D( floor( pixScales.y.mul( position.xyz ) ) ),
41+
).toVar( 'alpha' );
42+
43+
// Factor to interpolate lerp with
44+
const lerpFactor = fract( log2( pixScale ) ).toVar( 'lerpFactor' );
45+
46+
// Interpolate alpha threshold from noise at two scales
47+
const x = add( mul( lerpFactor.oneMinus(), alpha.x ), mul( lerpFactor, alpha.y ) ).toVar( 'x' );
48+
49+
// Pass into CDF to compute uniformly distrib threshold
50+
const a = min( lerpFactor, lerpFactor.oneMinus() ).toVar( 'a' );
51+
const cases = vec3(
52+
x.mul( x ).div( mul( 2.0, a ).mul( sub( 1.0, a ) ) ),
53+
x.sub( mul( 0.5, a ) ).div( sub( 1.0, a ) ),
54+
sub( 1.0, sub( 1.0, x ).mul( sub( 1.0, x ) ).div( mul( 2.0, a ).mul( sub( 1.0, a ) ) ) ) ).toVar( 'cases' );
55+
56+
// Find our final, uniformly distributed alpha threshold (ατ)
57+
const threshold = x.lessThan( a.oneMinus() ).select( x.lessThan( a ).select( cases.x, cases.y ), cases.z );
58+
59+
// Avoids ατ == 0. Could also do ατ =1-ατ
60+
return clamp( threshold, 1.0e-6, 1.0 );
61+
62+
} );
63+
64+
export default getAlphaHashThreshold;

test/e2e/puppeteer.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ const exceptionList = [
8989
'webgl_loader_texture_lottie',
9090
'webgl_loader_texture_pvrtc',
9191
'webgl_materials_alphahash',
92+
'webgpu_materials_alphahash',
9293
'webgl_materials_blending',
9394
'webgl_mirror',
9495
'webgl_morphtargets_face',

0 commit comments

Comments
 (0)