Particles not visible, no errors, suspecting shader + code review request

Hi,
I’ve started learning about Three.JS particles from articles, tutorials and looking at the code of others. I produced the following code (in React), which, from what I understood SHOULD be working.
I would like to ask you to review it with focus on why my particles are not visible at all. I suspect shaders, which I’m not fully understanding yet.
I would also appreciate any tips for optimization or good practices, since it’s my first project of sort.

componentDidMount = () => {
            this.init();

        }

        init = () => {

            const rand = (min,max) => min + Math.random()*(max-min);

            const canvas = this.canvasRef.current;

            this.color = new THREE.Color();

            //scene

            this.scene = new THREE.Scene();

           // this.texture = this.generateTexture();

            //renderer

            this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true });

            this.renderer.setClearColor(0x666666);

            this.renderer.setSize( window.innerWidth, window.innerHeight )

            document.body.appendChild(this.renderer.domElement);

            //camera

            this.camera = new THREE.PerspectiveCamera(100, 0, 0.0001, 10000);

              this.camera.position.z = 30; 

              this.camera.position.x = 0;

              this.camera.position.y = 0;

              this.camera.updateProjectionMatrix();

              this.controls = new THREE.OrbitControls(this.camera, this.renderer.domElement);

              this.controls.enableDamping = true;

              this.controls.dampingFactor = 0.2;

              this.controls.enableKeys = false;

     

            this.gridHelper = new THREE.GridHelper(300, 60, 0xffffff, 0xffffff);

            this.gridHelper.material.transparent = true;

            this.gridHelper.material.opacity = .3;

            this.scene.add(this.gridHelper);

            this.axisHelper = new AxisHelper(150, .6);

            this.scene.add(this.axisHelper);

            this.camera.lookAt(new THREE.Vector3());

              //lights

              let hemiLight = new THREE.HemisphereLight(0xffffff,  0.91);

                hemiLight.position.set(0, 50, 0);

                // Add hemisphere light to scene

                this.scene.add(hemiLight);

        

                // geometry

                this.geometry = new THREE.BufferGeometry();

                this.particles = 10;

                const radius = 200;

                this.positions = new Float32Array(this.particles * 3);

                this.colors = new Float32Array(this.particles * 3);

                this.sizes = new Float32Array(this.particles);

                this.size = 1000;

                this.parts = [];

                //vec3 attributes

                this.geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( this.positions, 3 ) );

                this.geometry.setAttribute( 'color', new THREE.Float32BufferAttribute( this.colors, 3 ) );

                this.geometry.setAttribute( 'size', new THREE.Float32BufferAttribute( this.sizes, 1 ));

           

                for ( var i = 0; i < this.particles; i ++ ) {

                    let size = rand(10, 80);

                    this.color.setHSL( i / this.particles, 1.0, 0.5 );

                    let part = {

                        offset: 0,

                        position: new THREE.Vector3(

                            rand(-this.size / 2, this.size / 2),

                            rand(-this.size / 2, this.size / 2),

                            rand(-this.size / 2, this.size / 2)

                        ),

                        baseSize: size,

                        size: size,

                        r: this.color.r,

                        g: this.color.g,

                        b: this.color.b,

                        a: 0.6,

                        life: 2,

                        decay: rand(0.05, 0.15),

                        firstRun: true

                    };

                    this.parts.push(part);

                }

                     

                this.material = new THREE.ShaderMaterial( {

                    uniforms: {

                        pointTexture: { value: new THREE.TextureLoader().load( "../3d/particle.png" ) }

                        

                    },

                    vertexShader: particlesVertexShader,

                    fragmentShader: particlesFragmentShader,

                    blending: THREE.AdditiveBlending,

                    depthTest: false,

                    transparent: true,

                    vertexColors: true

                 });

                this.particleSystem = new THREE.Points(this.geometry, this.material);

                this.scene.add(this.particleSystem);

                this.updateParticleAttributes(true, true, true);

               

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

                this.update();

        }

        generateTexture=() =>{

            let c = document.createElement('canvas');

            let ctx = c.getContext('2d');

            let size = 256;

            c.width = size;

            c.height = size;

            let gradient = ctx.createRadialGradient(size * 0.5, size * 0.5, 0, size * 0.5, size * 0.5, size * 0.4);

            gradient.addColorStop(0, 'hsla(100, 80%, 60%, 1)');

            gradient.addColorStop(1, 'hsla(100, 80%, 60%, 0.6 )');

            ctx.beginPath();

            ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);

            ctx.fillStyle = gradient;

            ctx.fill();

            let texture = new THREE.Texture(c);

            texture.needsUpdate = true;

            return texture;

        }

        updateParticleAttributes=(color, position, size) =>{

            let i = 0;

            while(i<this.particles) {

                let part = this.parts[i];

                if(color) {

                    this.colors[i * 4 + 0] = part.r;

                    this.colors[i * 4 + 1] = part.g;

                    this.colors[i * 4 + 2] = part.b;

                }

                if(position) {

                    this.positions[i * 3 + 0] = part.position.x;

                    this.positions[i * 3 + 1] = part.position.y;

                    this.positions[i * 3 + 2] = part.position.z;

                }

                if(size) {

                    this.sizes[i] = part.size;

                }

                i++;

            }

            if(color) {

                this.geometry.attributes.color.needsUpdate = true;

            }

            if(position) {

                this.geometry.attributes.position.needsUpdate = true;

            }

            if(size) {

                this.geometry.attributes.size.needsUpdate = true;

            }

            this.geometry.computeBoundingSphere();

        }

        update=()=>{

            requestAnimationFrame(this.update);

            const rand = (min,max) => min + Math.random()*(max-min);

              let noiseTime = this.clock.getElapsedTime() * 0.0008;

              let noiseVelocity = this.simplex.noise2D(0,1);

             const noiseScale = 0.01;

            for ( var i = 0; i < this.particles; i ++ ) {

                let part = this.parts[i];

                // let xScaled = part.position.x * noiseScale;

                // let yScaled = part.position.y * noiseScale;

                // let zScaled = part.position.z * noiseScale;

                // let noise1 = this.simplex.noise4D(

                //     xScaled ,

                //  yScaled,

                //  zScaled,

                //  50 + noiseTime

                // )* 0.5 + 0.5;

                // let noise2 = this.simplex.noise4D(

                //     xScaled +100,

                //  yScaled+100,

                //  zScaled+100,

                //  50 + noiseTime

                // )* 0.5 + 0.5;

                // let noise3 = this.simplex.noise4D(

                //     xScaled +200,

                //  yScaled+200,

                //  zScaled+200,

                //  50 + noiseTime

                // )* 0.5 + 0.5;

                // part.position.x += Math.sin(noise1 * Math.PI * 2) * this.clock.getDelta() * noiseVelocity;

                // part.position.y += Math.sin(noise2 * Math.PI * 2) * this.clock.getDelta() * noiseVelocity;

                // part.position.z += Math.sin(noise3 * Math.PI * 2) * this.clock.getDelta() * noiseVelocity;

                // this.parts[i] = part;

                if(part.life <= 0 || part.firstRun) {

                    part.life = 2;

                    part.position.x = rand(-this.size / 2, this.size / 2);

                    part.position.y = rand(-this.size / 2, this.size / 2);

                    part.position.z = rand(-this.size / 2, this.size / 2);

                    let hue = (this.clock.elapsedTime / 25 + rand(90)) % 360 + 110;

                    let lightness = Math.round(rand(10, 50));

                    this.color.set(`hsl(${hue}, 85%, ${lightness}%)`);

                    part.r = parseInt(this.color.r)+100;

                    part.g = parseInt(this.color.g);

                    part.b = parseInt(this.color.b);

                    part.firstRun = false;

                }

                this.parts[i] = part;

            }

            this.updateParticleAttributes(true, true, true);

            this.renderer.render(this.scene, this.camera);

                   

        }

        onWindowResize=()=> {

            this.camera.aspect = window.innerWidth / window.innerHeight;

            this.camera.updateProjectionMatrix();

            this.renderer.setSize( window.innerWidth, window.innerHeight );

        }

        render=()=>{

            return <div>

                <canvas ref={ref=>{this.canvasRef = ref}}></canvas>

            </div>

        }

In the code above the animating part is commented since I was trying to make it work on the inital render first. Animation does not work either, however I can see it is calculating in Performace tab.
The Vertex shader:

   export default 'attribute float size; \

    varying vec3 vColor; \

    void main() {\

        vColor = color;\

        vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );\

        gl_PointSize = size * ( 300.0 / -mvPosition.z );\

        gl_Position = projectionMatrix * mvPosition;\

    }';

The fragment shader:

export default 'uniform sampler2D pointTexture;\

                    varying vec3 vColor;\

                    void main() {\

                    gl_FragColor = vec4( vColor, 0.8 );\

                    gl_FragColor = gl_FragColor * texture2D( pointTexture, gl_PointCoord ); \

                    }';

Many thanks in advance.

Any chances to share a live demo for debugging? I can’t see something obvious in your code.

Sure, should’ve done that first:

I’ve copied the code to a fiddle since it was easier for me to make my changes:

https://jsfiddle.net/9zdjf1xq/8/

First of all, my code is not perfect yet. I just had no time anymore to further work on this, sorry^^. But at least you have now something workable. Some important points:

  • You camera settings were wrong. You have set a 0 aspect ratio which does not work.
  • The general HTML structure of your demo was wrong since you added two canvas elements to the DOM.
  • You produced NaN values when computing hue since you just passed only one parameter to your rand() function.
  • You always have to update the geometrie’s attribute data, not the arrays you have passed in in the constructor. Meaning when you do this:
this.geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( this.positions, 3 ) 

Updating the values in this.positions won’t have any effect since the ctor will create a new internal array based on the given data.

In general, it is no good approach to adapt complex code and try to get it to work. It’s much better to start with a small and simple app and enhance it step by step. Your code had too many issues at once and a beginner will massively struggle to get everything to work.

4 Likes

Thank you so much! At first I thought it could be an issue with updating the attributes, but I couldn’t really figure how to do it properly. But many other things were wrong too.

I’ve learned a lot, many thanks :slight_smile: