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