diff --git a/examples/files.json b/examples/files.json
index 7f401d6dfeebde..9a26cabfafecbc 100644
--- a/examples/files.json
+++ b/examples/files.json
@@ -326,6 +326,7 @@
"webgpu_equirectangular",
"webgpu_instance_mesh",
"webgpu_instance_points",
+ "webgpu_instance_points_billboards",
"webgpu_instance_uniform",
"webgpu_instancing_morph",
"webgpu_lightprobe",
diff --git a/examples/jsm/geometries/InstancedPointsGeometry.js b/examples/jsm/geometries/InstancedPointsGeometry.js
index 081ab6b81af176..184c367879d90c 100644
--- a/examples/jsm/geometries/InstancedPointsGeometry.js
+++ b/examples/jsm/geometries/InstancedPointsGeometry.js
@@ -18,9 +18,8 @@ class InstancedPointsGeometry extends InstancedBufferGeometry {
this.isInstancedPointsGeometry = true;
this.type = 'InstancedPointsGeometry';
-
const positions = [ - 1, 1, 0, 1, 1, 0, - 1, - 1, 0, 1, - 1, 0 ];
- const uvs = [ - 1, 1, 1, 1, - 1, - 1, 1, - 1 ];
+ const uvs = [ 0, 1, 1, 1, 0, 0, 1, 0 ];
const index = [ 0, 2, 1, 2, 3, 1 ];
this.setIndex( index );
diff --git a/examples/screenshots/webgpu_instance_points_billboards.jpg b/examples/screenshots/webgpu_instance_points_billboards.jpg
new file mode 100644
index 00000000000000..f046cff004ab65
Binary files /dev/null and b/examples/screenshots/webgpu_instance_points_billboards.jpg differ
diff --git a/examples/webgpu_instance_points.html b/examples/webgpu_instance_points.html
index 171c6c09456d43..b2aeabfab8d51c 100644
--- a/examples/webgpu_instance_points.html
+++ b/examples/webgpu_instance_points.html
@@ -142,6 +142,7 @@
pointWidth: 10, // in pixel units
vertexColors: true,
alphaToCoverage: true,
+ sizeAttenuation: false
} );
diff --git a/examples/webgpu_instance_points_billboards.html b/examples/webgpu_instance_points_billboards.html
new file mode 100644
index 00000000000000..6bf31142a1605b
--- /dev/null
+++ b/examples/webgpu_instance_points_billboards.html
@@ -0,0 +1,161 @@
+
+
+
+ three.js webgpu - particles - billboards
+
+
+
+
+
+
+
+
three.js - webgpu particle billboards example
+
+
+
+
+
+
+
diff --git a/src/materials/nodes/InstancedPointsNodeMaterial.js b/src/materials/nodes/InstancedPointsNodeMaterial.js
index 30d268c9046498..816ea87c77ece1 100644
--- a/src/materials/nodes/InstancedPointsNodeMaterial.js
+++ b/src/materials/nodes/InstancedPointsNodeMaterial.js
@@ -1,11 +1,11 @@
import NodeMaterial from './NodeMaterial.js';
import { attribute } from '../../nodes/core/AttributeNode.js';
import { cameraProjectionMatrix } from '../../nodes/accessors/Camera.js';
-import { materialColor, materialOpacity, materialPointWidth } from '../../nodes/accessors/MaterialNode.js'; // or should this be a property, instead?
+import { materialAlphaTest, materialColor, materialOpacity, materialPointWidth } from '../../nodes/accessors/MaterialNode.js'; // or should this be a property, instead?
import { modelViewMatrix } from '../../nodes/accessors/ModelNode.js';
-import { positionGeometry } from '../../nodes/accessors/Position.js';
-import { smoothstep, lengthSq } from '../../nodes/math/MathNode.js';
-import { Fn, vec4, float } from '../../nodes/tsl/TSLBase.js';
+import { positionGeometry, positionLocal, positionView } from '../../nodes/accessors/Position.js';
+import { lengthSq, smoothstep } from '../../nodes/math/MathNode.js';
+import { float, Fn, If, vec4 } from '../../nodes/tsl/TSLBase.js';
import { uv } from '../../nodes/accessors/UV.js';
import { viewport } from '../../nodes/display/ViewportNode.js';
@@ -27,6 +27,10 @@ class InstancedPointsNodeMaterial extends NodeMaterial {
this.lights = false;
+ this.sizeAttenuation = true;
+
+ this.useSizeAttenuation = true;
+
this.useAlphaToCoverage = true;
this.useColor = params.vertexColors;
@@ -39,78 +43,84 @@ class InstancedPointsNodeMaterial extends NodeMaterial {
this.setDefaultValues( _defaultValues );
- this.setupShaders();
-
this.setValues( params );
}
setup( builder ) {
- this.setupShaders();
+ this.setupShaders( builder );
super.setup( builder );
}
- setupShaders() {
+ setupShaders( { renderer } ) {
const useAlphaToCoverage = this.alphaToCoverage;
+ const useSizeAttenuation = this.sizeAttenuation;
const useColor = this.useColor;
this.vertexNode = Fn( () => {
const instancePosition = attribute( 'instancePosition' ).xyz;
- // camera space
- const mvPos = vec4( modelViewMatrix.mul( vec4( instancePosition, 1.0 ) ) );
+ positionLocal.assign( positionGeometry.add( instancePosition ) );
- const aspect = viewport.z.div( viewport.w );
+ const viewPosition = modelViewMatrix.mul( vec4( instancePosition, 1.0 ) );
+ positionView.assign( viewPosition );
- // clip space
- const clipPos = cameraProjectionMatrix.mul( mvPos );
+ const clipPos = cameraProjectionMatrix.mul( viewPosition );
+ const offset = positionGeometry.xy;
- // offset in ndc space
- const offset = positionGeometry.xy.toVar();
+ let size = this.pointWidthNode || materialPointWidth;
- offset.mulAssign( this.pointWidthNode ? this.pointWidthNode : materialPointWidth );
+ if ( useSizeAttenuation ) {
- offset.assign( offset.div( viewport.z ) );
- offset.y.assign( offset.y.mul( aspect ) );
+ // Convert size (diameter) to radius and apply perspective scaling
+ size = size.div( clipPos.w.div( viewport.w ) ).div( 2 );
- // back to clip space
- offset.assign( offset.mul( clipPos.w ) );
+ }
- //clipPos.xy += offset;
- clipPos.addAssign( vec4( offset, 0, 0 ) );
+ const adjustedOffset = offset.mul( size )
+ .div( viewport.zw )
+ .mul( clipPos.w );
- return clipPos;
+ return clipPos.add( vec4( adjustedOffset, 0, 0 ) );
} )();
this.fragmentNode = Fn( () => {
+ // force assignment into correct place in flow
const alpha = float( 1 ).toVar();
- const len2 = lengthSq( uv() );
+ If( materialAlphaTest.equal( 0.0 ), () => {
- if ( useAlphaToCoverage ) {
+ const len2 = lengthSq( uv().mul( 2 ).sub( 1 ) );
- const dlen = float( len2.fwidth() ).toVar();
- alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
+ if ( useAlphaToCoverage && renderer.samples > 1 ) {
- } else {
+ const dlen = float( len2.fwidth() ).toVar();
- len2.greaterThan( 1.0 ).discard();
+ alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() );
+
+ } else {
+
+ len2.greaterThan( 1.0 ).discard();
+
+ }
+
+ } );
- }
- let pointColorNode;
+
+ const output = vec4( 1 ).toVar();
if ( this.pointColorNode ) {
- pointColorNode = this.pointColorNode;
+ output.assign( this.pointColorNode );
} else {
@@ -118,19 +128,29 @@ class InstancedPointsNodeMaterial extends NodeMaterial {
const instanceColor = attribute( 'instanceColor' );
- pointColorNode = instanceColor.mul( materialColor );
+ output.assign( instanceColor.mul( materialColor ) );
} else {
- pointColorNode = materialColor;
+ output.assign( materialColor );
}
}
- alpha.mulAssign( materialOpacity );
+ output.a.mulAssign( alpha );
+ output.a.mulAssign( materialOpacity );
+
+
+ If( materialAlphaTest.greaterThan( 0.0 ), () => {
+
+ output.a.lessThanEqual( materialAlphaTest ).discard();
- return vec4( pointColorNode, alpha );
+ } );
+
+
+
+ return output;
} )();
@@ -153,6 +173,23 @@ class InstancedPointsNodeMaterial extends NodeMaterial {
}
+ get sizeAttenuation() {
+
+ return this.useSizeAttenuation;
+
+ }
+
+ set sizeAttenuation( value ) {
+
+ if ( this.useSizeAttenuation !== value ) {
+
+ this.useSizeAttenuation = value;
+ this.needsUpdate = true;
+
+ }
+
+ }
+
}
export default InstancedPointsNodeMaterial;
diff --git a/src/materials/nodes/NodeMaterial.js b/src/materials/nodes/NodeMaterial.js
index 667c9961c5fdbb..b40565829ecc0d 100644
--- a/src/materials/nodes/NodeMaterial.js
+++ b/src/materials/nodes/NodeMaterial.js
@@ -183,7 +183,9 @@ class NodeMaterial extends Material {
if ( globalClippingCount || localClippingCount ) {
- if ( this.alphaToCoverage ) {
+ const samples = builder.renderer.samples;
+
+ if ( this.alphaToCoverage && samples > 1 ) {
// to be added to flow when the color/alpha value has been determined
result = clippingAlpha();
diff --git a/src/renderers/webgl-fallback/utils/WebGLState.js b/src/renderers/webgl-fallback/utils/WebGLState.js
index 3993a175427558..feedf240c877f5 100644
--- a/src/renderers/webgl-fallback/utils/WebGLState.js
+++ b/src/renderers/webgl-fallback/utils/WebGLState.js
@@ -512,7 +512,7 @@ class WebGLState {
this.setPolygonOffset( material.polygonOffset, material.polygonOffsetFactor, material.polygonOffsetUnits );
- material.alphaToCoverage === true
+ material.alphaToCoverage === true && this.backend.renderer.samples > 1
? this.enable( gl.SAMPLE_ALPHA_TO_COVERAGE )
: this.disable( gl.SAMPLE_ALPHA_TO_COVERAGE );
diff --git a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js
index 6c749586b757b4..ef1a78dcfb0f3c 100644
--- a/src/renderers/webgpu/utils/WebGPUPipelineUtils.js
+++ b/src/renderers/webgpu/utils/WebGPUPipelineUtils.js
@@ -138,7 +138,7 @@ class WebGPUPipelineUtils {
},
multisample: {
count: sampleCount,
- alphaToCoverageEnabled: material.alphaToCoverage
+ alphaToCoverageEnabled: material.alphaToCoverage && sampleCount > 1
},
layout: device.createPipelineLayout( {
bindGroupLayouts