Is it possible to vary material parameters for a BufferGeometry?

I am trying to create a moving triangulation-effect backdrop for my website, and I would like every to vary the “size”-attribute of the material I am using for each white point between 0.3 and 2. Currently they are all set to size:1 as seen in the code below.

Here is the current code for showing the points:

var sz = { x: 200, z: 200 };
var pointsCount = 1000;
var points3d = [];
for (let i = 0; i < pointsCount; i++) {
  let x = THREE.MathUtils.randFloatSpread(sz.x);
  let z = THREE.MathUtils.randFloatSpread(sz.z);
  let y = 10;
  points3d.push(new THREE.Vector3(x, y, z));

const sprite = new THREE.TextureLoader().load( 'disc.png' );
var geom = new THREE.BufferGeometry().setFromPoints(points3d);
var material = new THREE.PointsMaterial( { size: 1, sizeAttenuation: true, map: sprite, alphaTest: 0.5, transparent: true } );

var cloud = new THREE.Points(
cloud.rotation.x = -Math.PI / 2; 

Current image:

Currently I am using sprites for this as billboarding is enough, but perhaps this is not the correct approach if I want varied size on the white dots?

Yes, it is possible. Modify cloud.material.size at run-time.

Ah, sorry, maybe I formulated it badly. Is it possible to have element-wise sizes within the same buffer? For example one circle has size 0.5 whilst another has 2.0 and a third one 1.2 or something. In other words 2 billboarded stars from the same buffer having different size-parameters.

Oh, you mean this:

I don’t know a simple way to do this with one cloud of points. All I can think of needs either more objects, or custom shader, or visual tricks, or instancing of circles/spheres.

Exactly! I see, I am running into the same issue, it feels like the cheapskate option with one buffer and billboarding is not enough for this :sweat_smile: I would later want to use the mesh for triangulation, so i would like to have it in one buffer for that. In theory maybe I could instantiate 4 or 5 buffers with dots of specific sizes, and then it if lets me, add them to the same BufferGeometry to render, do you know if this is possible?

The video above uses one cloud + visual tricks. Each point moves forward and backward along the line to the camera, so the point changes its size, but remains on the same position on the screen.

Several clouds sounds plausible, but you have to manage the transfer of points from one cloud to another one. Is this worth the headache?

Try instancing. 1000 points should be no problem.

Modified PointsMaterial is also an option :thinking:

I’m not familiar with Three.js internals, but if size becomes an array of sizes, there must be some gl.enableVertexAttribArray(...) command, so that each vertex has its own size. Can this be done by a modified PointsMaterial?

Create buffer attributes, add them to geometry and process them in modified material’s shaders.
For example: Using shader x, y, z coordinates relative to the tilted object and not the scene in Three JS - #3 by prisoner849

And also: three.js examples


Nice! I tried @PavelBoytchev’s solution, but it becomes a bit tricky as I want to use the same points for triangulation, so moving the points back to change their size messes with the triangulation unless i make a second buffer, but this makes things convoluted later on in the code and the depth messes with the billboards. Will try your solution with PointsMaterial when I get back from work, the Nova animation looks like it would achieve what I would like concerning the point sizes. Thank you guys!

Just what I wanted!

If someone is curious or want a similar effect, here is code:

let gu = {
  time: {value: 0}

let sizes = [];
let shift = [];
let pushShift = () => {
    Math.random() * Math.PI, 
    Math.random() * Math.PI * 2, 
    (Math.random() * 0.9 + 0.1) * Math.PI * 0.1,
    Math.random() * 0.9 + 0.1
let pts = new Array(10000).fill().map(p => {
  sizes.push(Math.random() * 1.5 + 0.5);
  var sz = { x: 230, y: 230 };
  let x = THREE.MathUtils.randFloatSpread(sz.x);
  let y = THREE.MathUtils.randFloatSpread(sz.y);
  let z = -30;
  return new THREE.Vector3(x,y,z);

let g = new THREE.BufferGeometry().setFromPoints(pts);
g.setAttribute("sizes", new THREE.Float32BufferAttribute(sizes, 1));
g.setAttribute("shift", new THREE.Float32BufferAttribute(shift, 4));
let m = new THREE.PointsMaterial({
  size: 0.5,
  transparent: true,
  depthTest: true,
  blending: THREE.AdditiveBlending,
  onBeforeCompile: shader => {
    shader.uniforms.time = gu.time;
    shader.vertexShader = `
      uniform float time;
      attribute float sizes;
      attribute vec4 shift;
      varying vec3 vColor;
      `gl_PointSize = size;`,
      `gl_PointSize = size * sizes;`
      `#include <color_vertex>`,
      `#include <color_vertex>
        float d = length(abs(position) / vec3(40., 10., 40));
        d = clamp(d, 0., 1.);
        vColor = mix(vec3(227., 155., 0.), vec3(100., 50., 255.), d) / 255.;
    shader.fragmentShader = `
      varying vec3 vColor;
      `#include <clipping_planes_fragment>`,
      `#include <clipping_planes_fragment>
        float d = length(gl_PointCoord.xy - 0.5);
        //if (d > 0.5) discard;
      `vec4 diffuseColor = vec4( diffuse, opacity );`,
      `vec4 diffuseColor = vec4( vColor, smoothstep(0.5, 0.1, d)/* * 0.5 + 0.5*/ );`
let cloud = new THREE.Points(g, m);
cloud.rotation.order = "ZYX";
cloud.rotation.z = 0.0;


var indexDelaunay = Delaunator.from( => {
    return [v.x, v.y];

var meshIndex = []; // delaunay index => three.js index
for (let i = 0; i < indexDelaunay.triangles.length; i++){

g.setIndex(meshIndex); // add three.js index to the existing geometry

var mesh = new THREE.Mesh(
  new THREE.MeshLambertMaterial({ color: "purple", wireframe: true })