diff --git a/examples/files.json b/examples/files.json index c7e741009227da..35a57344d5908c 100644 --- a/examples/files.json +++ b/examples/files.json @@ -338,6 +338,7 @@ "webgpu_morphtargets", "webgpu_occlusion", "webgpu_particles", + "webgpu_points_fat", "webgpu_rtt", "webgpu_sandbox", "webgpu_shadowmap", diff --git a/examples/jsm/nodes/Nodes.js b/examples/jsm/nodes/Nodes.js index 5760f34d4229cd..57112778a04735 100644 --- a/examples/jsm/nodes/Nodes.js +++ b/examples/jsm/nodes/Nodes.js @@ -72,6 +72,7 @@ export { default as BufferNode, buffer } from './accessors/BufferNode.js'; export { default as CameraNode, cameraProjectionMatrix, cameraViewMatrix, cameraNormalMatrix, cameraWorldMatrix, cameraPosition, cameraNear, cameraFar } from './accessors/CameraNode.js'; export { default as CubeTextureNode, cubeTexture } from './accessors/CubeTextureNode.js'; export { default as ExtendedMaterialNode, materialNormal } from './accessors/ExtendedMaterialNode.js'; +export { default as FatPointsMaterialNode, materialPointWidth } from './accessors/FatPointsMaterialNode.js'; export { default as InstanceNode, instance } from './accessors/InstanceNode.js'; export { default as LineMaterialNode, materialLineDashSize, materialLineDashOffset, materialLineGapSize, materialLineScale, materialLineWidth } from './accessors/LineMaterialNode.js'; export { default as MaterialNode, materialAlphaTest, materialColor, materialShininess, materialEmissive, materialOpacity, materialSpecularColor, materialReflectivity, materialRoughness, materialMetalness, materialRotation, materialSheen, materialSheenRoughness } from './accessors/MaterialNode.js'; diff --git a/examples/jsm/nodes/accessors/FatPointsMaterialNode.js b/examples/jsm/nodes/accessors/FatPointsMaterialNode.js new file mode 100644 index 00000000000000..793a62ba7e49fa --- /dev/null +++ b/examples/jsm/nodes/accessors/FatPointsMaterialNode.js @@ -0,0 +1,21 @@ +import MaterialNode from './MaterialNode.js'; +import { addNodeClass } from '../core/Node.js'; +import { nodeImmutable } from '../shadernode/ShaderNode.js'; + +class FatPointsMaterialNode extends MaterialNode { + + setup( /*builder*/ ) { + + return this.getFloat( this.scope ); + + } + +} + +FatPointsMaterialNode.POINTWIDTH = 'pointWidth'; + +export default FatPointsMaterialNode; + +export const materialPointWidth = nodeImmutable( FatPointsMaterialNode, FatPointsMaterialNode.POINTWIDTH ); + +addNodeClass( 'FatPointsMaterialNode', FatPointsMaterialNode ); diff --git a/examples/jsm/nodes/core/PropertyNode.js b/examples/jsm/nodes/core/PropertyNode.js index 4eab386c5ce087..781edc63879395 100644 --- a/examples/jsm/nodes/core/PropertyNode.js +++ b/examples/jsm/nodes/core/PropertyNode.js @@ -55,5 +55,6 @@ export const shininess = nodeImmutable( PropertyNode, 'float', 'Shininess' ); export const output = nodeImmutable( PropertyNode, 'vec4', 'Output' ); export const dashSize = nodeImmutable( PropertyNode, 'float', 'dashSize' ); export const gapSize = nodeImmutable( PropertyNode, 'float', 'gapSize' ); +export const pointWidth = nodeImmutable( PropertyNode, 'float', 'pointWidth' ); addNodeClass( 'PropertyNode', PropertyNode ); diff --git a/examples/jsm/nodes/materials/FatPointsNodeMaterial.js b/examples/jsm/nodes/materials/FatPointsNodeMaterial.js new file mode 100644 index 00000000000000..4c779490f8f92a --- /dev/null +++ b/examples/jsm/nodes/materials/FatPointsNodeMaterial.js @@ -0,0 +1,164 @@ +import NodeMaterial, { addNodeMaterial } from './NodeMaterial.js'; +import { varying } from '../core/VaryingNode.js'; +import { property } from '../core/PropertyNode.js'; +import { attribute } from '../core/AttributeNode.js'; +import { cameraProjectionMatrix } from '../accessors/CameraNode.js'; +import { materialColor } from '../accessors/MaterialNode.js'; +import { modelViewMatrix } from '../accessors/ModelNode.js'; +import { positionGeometry } from '../accessors/PositionNode.js'; +import { smoothstep } from '../math/MathNode.js'; +import { tslFn, vec2, vec4 } from '../shadernode/ShaderNode.js'; +import { uv } from '../accessors/UVNode.js'; +import { materialPointWidth } from '../accessors/FatPointsMaterialNode.js'; // or should this be a property, instead? +import { viewport } from '../display/ViewportNode.js'; +import { color } from 'three/nodes'; + +import { PointsMaterial } from 'three'; + +const defaultValues = new PointsMaterial(); + +class FatPointsNodeMaterial extends NodeMaterial { + + constructor( params = {} ) { + + super(); + + this.normals = false; + + this.lights = false; + + this.useAlphaToCoverage = true; + + this.useColor = params.vertexColors; + + this.pointWidth = 1; + + this.pointColorNode = null; + + this.setDefaultValues( defaultValues ); + + this.setupShaders(); + + this.setValues( params ); + + } + + setupShaders() { + + const useAlphaToCoverage = this.alphaToCoverage; + const useColor = this.useColor; + + this.vertexNode = tslFn( () => { + + //vUv = uv; + varying( vec2(), 'vUv' ).assign( uv() ); // @TODO: Analyze other way to do this + + const instancePosition = attribute( 'instancePosition' ); + + // camera space + const mvPos = property( 'vec4', 'mvPos' ); + mvPos.assign( modelViewMatrix.mul( vec4( instancePosition, 1.0 ) ) ); + + const aspect = viewport.z.div( viewport.w ); + + // clip space + const clipPos = cameraProjectionMatrix.mul( mvPos ); + + // offset in ndc space + const offset = property( 'vec2', 'offset' ); + offset.assign( positionGeometry.xy ); + offset.assign( offset.mul( materialPointWidth ) ); + offset.assign( offset.div( viewport.z ) ); + offset.y.assign( offset.y.mul( aspect ) ); + + // back to clip space + offset.assign( offset.mul( clipPos.w ) ); + + //clipPos.xy += offset; + clipPos.assign( clipPos.add( vec4( offset, 0, 0 ) ) ); + + return clipPos; + + //vec4 mvPosition = mvPos; // this was used for somethihng... + + } )(); + + this.colorNode = tslFn( () => { + + const vUv = varying( vec2(), 'vUv' ); + + // force assignment into correct place in flow + const alpha = property( 'float', 'alpha' ); + alpha.assign( 1 ); + + const a = vUv.x; + const b = vUv.y; + + const len2 = a.mul( a ).add( b.mul( b ) ); + + if ( useAlphaToCoverage ) { + + // force assignment out of following 'if' statement - to avoid uniform control flow errors + const dlen = property( 'float', 'dlen' ); + dlen.assign( len2.fwidth() ); + + alpha.assign( smoothstep( dlen.oneMinus(), dlen.add( 1 ), len2 ).oneMinus() ); + + } else { + + len2.greaterThan( 1.0 ).discard(); + + } + + let pointColorNode; + + if ( this.pointColorNode ) { + + pointColorNode = this.pointColorNode; + + } else { + + if ( useColor ) { + + const instanceColor = attribute( 'instanceColor' ); + + pointColorNode = color( instanceColor ).mul( color( materialColor ) ); + + } else { + + pointColorNode = materialColor; + + } + + } + + return vec4( pointColorNode, alpha ); + + } )(); + + this.needsUpdate = true; + + } + + get alphaToCoverage() { + + return this.useAlphaToCoverage; + + } + + set alphaToCoverage( value ) { + + if ( this.useAlphaToCoverage !== value ) { + + this.useAlphaToCoverage = value; + this.setupShaders(); + + } + + } + +} + +export default FatPointsNodeMaterial; + +addNodeMaterial( 'FatPointsNodeMaterial', FatPointsNodeMaterial ); diff --git a/examples/jsm/nodes/materials/Materials.js b/examples/jsm/nodes/materials/Materials.js index adab189b924034..5dcd6672ecc987 100644 --- a/examples/jsm/nodes/materials/Materials.js +++ b/examples/jsm/nodes/materials/Materials.js @@ -1,6 +1,7 @@ // @TODO: We can simplify "export { default as SomeNode, other, exports } from '...'" to just "export * from '...'" if we will use only named exports export { default as NodeMaterial, addNodeMaterial, createNodeMaterialFromType } from './NodeMaterial.js'; +export { default as FatPointsNodeMaterial } from './FatPointsNodeMaterial.js'; export { default as LineBasicNodeMaterial } from './LineBasicNodeMaterial.js'; export { default as LineDashedNodeMaterial } from './LineDashedNodeMaterial.js'; export { default as Line2NodeMaterial } from './Line2NodeMaterial.js'; diff --git a/examples/jsm/points/FatPoints.js b/examples/jsm/points/FatPoints.js new file mode 100644 index 00000000000000..784e87d72ac42a --- /dev/null +++ b/examples/jsm/points/FatPoints.js @@ -0,0 +1,21 @@ +import { + Mesh +} from 'three'; +import { FatPointsGeometry } from '../points/FatPointsGeometry.js'; +import { FatPointsNodeMaterial } from 'three/nodes'; + +class FatPoints extends Mesh { + + constructor( geometry = new FatPointsGeometry(), material = new FatPointsNodeMaterial() ) { + + super( geometry, material ); + + this.isFatPoints = true; + + this.type = 'FatPoints'; + + } + +} + +export { FatPoints }; diff --git a/examples/jsm/points/FatPointsGeometry.js b/examples/jsm/points/FatPointsGeometry.js new file mode 100644 index 00000000000000..1dcffd096f33ef --- /dev/null +++ b/examples/jsm/points/FatPointsGeometry.js @@ -0,0 +1,174 @@ +import { + Box3, + Float32BufferAttribute, + InstancedBufferGeometry, + InstancedBufferAttribute, + Sphere, + Vector3 +} from 'three'; + +const _vector = new Vector3(); + +class FatPointsGeometry extends InstancedBufferGeometry { + + constructor() { + + super(); + + this.isFatPointsGeometry = true; + + this.type = 'FatPointsGeometry'; + + 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 index = [ 0, 2, 1, 2, 3, 1 ]; + + this.setIndex( index ); + this.setAttribute( 'position', new Float32BufferAttribute( positions, 3 ) ); + this.setAttribute( 'uv', new Float32BufferAttribute( uvs, 2 ) ); + + } + + applyMatrix4( matrix ) { + + const pos = this.attributes.instancePosition; + + if ( pos !== undefined ) { + + pos.applyMatrix4( matrix ); + + pos.needsUpdate = true; + + } + + if ( this.boundingBox !== null ) { + + this.computeBoundingBox(); + + } + + if ( this.boundingSphere !== null ) { + + this.computeBoundingSphere(); + + } + + return this; + + } + + setPositions( array ) { + + let points; + + if ( array instanceof Float32Array ) { + + points = array; + + } else if ( Array.isArray( array ) ) { + + points = new Float32Array( array ); + + } + + this.setAttribute( 'instancePosition', new InstancedBufferAttribute( points, 3 ) ); // xyz + + // + + this.computeBoundingBox(); + this.computeBoundingSphere(); + + return this; + + } + + setColors( array ) { + + let colors; + + if ( array instanceof Float32Array ) { + + colors = array; + + } else if ( Array.isArray( array ) ) { + + colors = new Float32Array( array ); + + } + + this.setAttribute( 'instanceColor', new InstancedBufferAttribute( colors, 3 ) ); // rgb + + return this; + + } + + computeBoundingBox() { + + if ( this.boundingBox === null ) { + + this.boundingBox = new Box3(); + + } + + const pos = this.attributes.instancePosition; + + if ( pos !== undefined ) { + + this.boundingBox.setFromBufferAttribute( pos ); + + } + + } + + computeBoundingSphere() { + + if ( this.boundingSphere === null ) { + + this.boundingSphere = new Sphere(); + + } + + if ( this.boundingBox === null ) { + + this.computeBoundingBox(); + + } + + const pos = this.attributes.instancePosition; + + if ( pos !== undefined ) { + + const center = this.boundingSphere.center; + + this.boundingBox.getCenter( center ); + + let maxRadiusSq = 0; + + for ( let i = 0, il = pos.count; i < il; i ++ ) { + + _vector.fromBufferAttribute( pos, i ); + maxRadiusSq = Math.max( maxRadiusSq, center.distanceToSquared( _vector ) ); + + } + + this.boundingSphere.radius = Math.sqrt( maxRadiusSq ); + + if ( isNaN( this.boundingSphere.radius ) ) { + + console.error( 'THREE.FatPointsGeometry.computeBoundingSphere(): Computed radius is NaN. The instanced position data is likely to have NaN values.', this ); + + } + + } + + } + + toJSON() { + + // todo + + } + +} + +export { FatPointsGeometry }; diff --git a/examples/screenshots/webgpu_points_fat.jpg b/examples/screenshots/webgpu_points_fat.jpg new file mode 100644 index 00000000000000..c0ad77b069951d Binary files /dev/null and b/examples/screenshots/webgpu_points_fat.jpg differ diff --git a/examples/webgpu_points_fat.html b/examples/webgpu_points_fat.html new file mode 100644 index 00000000000000..802a274124053e --- /dev/null +++ b/examples/webgpu_points_fat.html @@ -0,0 +1,234 @@ + + +
+