html, body {
  margin: 0;
  padding: 0;
  overflow: hidden;
}
const vshader = `

    precision highp float;

    uniform mat4 modelViewMatrix;
    uniform mat4 projectionMatrix;
    uniform vec4 origin;
    uniform float time;

    attribute vec3 position;
    attribute vec3 translate;
    attribute vec3 color;
    attribute float brightness;
    attribute float saturation;
    attribute float sides;
    attribute float scale;
    attribute float thickness;

    varying float vThickness;
    varying float vSides;
    varying float vBrightness;
    varying float vSaturation;
    varying vec3 vColor;
    
    void main() {
        vColor = color;
        vSides = sides;
        vThickness = thickness;
        vBrightness = brightness;
        vSaturation = saturation;

        vec4 mvPosition = modelViewMatrix * vec4( translate, 1.0 );
        float cameraDist = distance( mvPosition, origin );

        mvPosition.xyz += position;

        gl_Position = projectionMatrix * mvPosition;
        gl_PointSize = (100.0 / (cameraDist / 100.0)) * scale;
    }
`;

const fshader = `

    #extension GL_OES_standard_derivatives : enable

    #define PI 3.14159265359
    #define TWO_PI 6.28318530718

    precision highp float;

    uniform float time;
    uniform float fogNear;
    uniform float fogFar;
    uniform vec3 fogColor;

    varying vec3 vColor;
    varying float vSides;
    varying float vThickness;
    varying float vBrightness;
    varying float vSaturation;

    vec3 rgb2hsb(vec3 c) {
        vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
        vec4 p = mix(vec4(c.bg, K.wz),
                     vec4(c.gb, K.xy),
                     step(c.b, c.g));
        vec4 q = mix(vec4(p.xyw, c.r),
                     vec4(c.r, p.yzx),
                     step(p.x, c.r));
        float d = q.x - min(q.w, q.y);
        float e = 1.0e-10;
        return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)),
                    d / (q.x + e),
                    q.x);
    }

    vec3 hsb2rgb(vec3 c) {
        vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0),
                                 6.0)-3.0)-1.0,
                         0.0,
                         1.0 );
        rgb = rgb*rgb*(3.0-2.0*rgb);
        return c.z * mix(vec3(1.0), rgb, c.y);
    }   

    float getShape(float thickness, float outer, vec2 uv){
        uv *= 2.0;
        float a = atan(uv.x,-uv.y) + PI;
        float r = TWO_PI / vSides;
        float d = cos( floor( .5 + a / r ) * r - a ) * length( uv );
        return smoothstep(thickness - fwidth(d), thickness + fwidth(d), d) - smoothstep(outer - fwidth(d), outer + fwidth(d), d);
    }

    void main() {
        vec2 uv = gl_PointCoord;
        uv -= 0.5;
    
        float color = 0.0;
        float shape = 0.0;

        float depth = gl_FragCoord.z / gl_FragCoord.w;
        float fogFactor = smoothstep( fogNear, fogFar, depth );

        float outer = 0.4;
        float newMax = 0.1;
        float newMin = outer - newMax;
        float inner = newMin + (newMax - newMin) * vThickness;
        
        shape = getShape(0.0, 0.5, uv);
        color = getShape(inner, outer, uv);

        if (shape < 0.5) discard;

        vec3 vColorRGB = vec3(vColor);
        vec3 vColorHSB = rgb2hsb(vColorRGB);
        
        vColorHSB.y *= vSaturation;
        vColorHSB.z *= vBrightness;
        vColorRGB = hsb2rgb(vColorHSB);

        vec3 baseColor = vec3(0, 0, 0);
        vec3 col = mix(baseColor, vColorRGB, color);

        gl_FragColor = mix( vec4(col, 1.0), vec4( fogColor, 1.0 ), fogFactor);
    }
    
`;

let camera, scene, renderer;
let points, material;
let controls;

init();
animate();

function init() {

    renderer = new THREE.WebGLRenderer({ alpha: false, antialias: false });
    renderer.setPixelRatio( window.devicePixelRatio );
    renderer.setSize( window.innerWidth, window.innerHeight );
    renderer.domElement.setAttribute("id", "scatterplot");
    document.body.appendChild(renderer.domElement);

    let geometry;
    let numPoints = 11000;
    let colors = new Float32Array(numPoints * 3);
    let translations = new Float32Array(numPoints * 3);
    let saturations = new Float32Array(numPoints);
    let brightnesses = new Float32Array(numPoints);
    let thicknesses = new Float32Array(numPoints);
    let scales = new Float32Array(numPoints);
    let numSides = new Float32Array(numPoints);
    let colorOptions = [
        { r: 159/255, g: 3/255,   b: 66/255  },
        { r: 228/255, g: 110/255, b: 73/255  },
        { r: 253/255, g: 223/255, b: 116/255 },
        { r: 66/255,  g: 234/255, b: 133/255 },
        { r: 19/255,  g: 174/255, b: 241/255 },
    ];
    let numSideOptions = [3.0, 4.0, 100.0];

    for (let i = 0; i < numPoints * 3; i += 3) {
        let color = colorOptions[Math.floor(Math.random() * colorOptions.length)];

        colors[i] = color.r;
        colors[i+1] = color.g;
        colors[i+2] = color.b;

        if (Math.random() <= 0.5) {
            Math.random() <= 0.5 ? translations[i] = -Math.random() * 100 : translations[i] = Math.random() * 100;
            Math.random() <= 0.5 ? translations[i+1] = -Math.random() * 100 : translations[i+1] = Math.random() * 100;
            Math.random() <= 0.5 ? translations[i+2] = -Math.random() * 100 : translations[i+2] = Math.random() * 100;
        } else {
            Math.random() <= 0.5 ? translations[i] = Math.random() * 100 : translations[i] = -Math.random() * 100;
            Math.random() <= 0.5 ? translations[i+1] = Math.random() * 100 : translations[i+1] = -Math.random() * 100;
            Math.random() <= 0.5 ? translations[i+2] = Math.random() * 100 : translations[i+2] = -Math.random() * 100;
        }
    }

    for (let i = 0; i < numPoints; i++) {
        saturations[i] = Math.max(0.75, Math.random());
        brightnesses[i] = Math.max(0.75, Math.random());
        numSides[i] = numSideOptions[Math.floor(Math.random() * numSideOptions.length)];
        numSides[i] === 3.0 ? scales[i] = Math.max(0.4, Math.random()) * 0.75 : scales[i] = Math.max(0.4, Math.random()); // this just scales down triangles slightly to make them visually "on par" with the other shapes given the same scale
        numSides[i] === 3.0 ? thicknesses[i] = Math.min(1.0, Math.random() * 1.25) : thicknesses[i] = Math.random(); // due to the scaling down of triangles, needed to amp up the thickness of the lines slightly to compensate
    }
    
    camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight );
    
    scene = new THREE.Scene();
    scene.fog = new THREE.Fog( 0x000000, 150, 500 );
    
    geometry = new THREE.InstancedBufferGeometry();

    geometry.addAttribute( 'position', new THREE.BufferAttribute( new Float32Array( [ 0, 0, 0 ] ), 3));

    geometry.addAttribute( 'brightness', new THREE.InstancedBufferAttribute( brightnesses, 1 ));
    geometry.addAttribute( 'saturation', new THREE.InstancedBufferAttribute( saturations, 1 ));
    geometry.addAttribute( 'thickness', new THREE.InstancedBufferAttribute( thicknesses, 1 ));
    geometry.addAttribute( 'sides', new THREE.InstancedBufferAttribute( numSides, 1 ));
    geometry.addAttribute( 'translate', new THREE.InstancedBufferAttribute( translations, 3 ));
    geometry.addAttribute( 'color', new THREE.InstancedBufferAttribute( colors, 3 ) );
    geometry.addAttribute( 'scale', new THREE.InstancedBufferAttribute( scales, 1 ) );
    
    material = new THREE.RawShaderMaterial({ 
        uniforms: {
            fogColor: { type: "c", value: scene.fog.color },
            fogNear: { type: "f", value: scene.fog.near },
            fogFar: { type: "f", value: scene.fog.far }
        },
        vertexShader: vshader,
        fragmentShader: fshader
    });

    points = new THREE.Points( geometry, material );
    scene.add( points );

    setControls();

    window.addEventListener( 'resize', onWindowResize, false );

    function setControls() {
        controls = new THREE.OrbitControls(camera, document.getElementById("scatterplot"));
        controls.enableDamping = true;
        controls.dampingFactor = 0.25;
        controls.mouseButtons = {
            LEFT: THREE.MOUSE.LEFT,
            MIDDLE: THREE.MOUSE.RIGHT
        }
        controls.rotateSpeed = 0.15;
        controls.target = new THREE.Vector3(0,0,0);
        controls.screenSpacePanning = false;
        controls.enablePan = false;
        controls.minDistance = 20;
        controls.maxDistance = 350;
        controls.maxPolarAngle = Math.PI;
        controls.minPolarAngle = 0;
    }
}

function animate() {
    requestAnimationFrame( animate );
    controls.update();
    render();
}

function render() {
    renderer.render( scene, camera );
}

function onWindowResize( event ) {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize( window.innerWidth, window.innerHeight );
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/101/three.min.js
  2. https://threejs.org/examples/js/controls/OrbitControls.js