Search code examples
three.jswebglrendering

three.js renders unexpected color values using points and custom shading and blending


I have a relatively simple setup with custom shader and a custom blend mode:

  • A Scene and a PerspectiveCamera
  • An IcosahedronBufferGeometry
  • A custom new THREE.ShaderMaterial with custom shaders
  • Some new THREE.Points with the icosahedron and that custom shader material

My shader material has a custom blend mode:

shaderMaterial.blending =      THREE.CustomBlending;
shaderMaterial.blendSrc =      THREE.OneFactor;
shaderMaterial.blendDst =      THREE.OneFactor;
shaderMaterial.blendEquation = THREE.AddEquation;

and my fragment shader currently only does this:

gl_FragColor = vec4(0.0, 0.1, 0.0, 0.0);

Since I'm rendering into a fully black canvas, the expected color of drawn points should be, considering the blending mode:

 SRC                     DST
r = 0                   r = 0                 r = 0
g = 0.1   *   1.0   +   g = 0   *   1.0   =   g = 0.1
b = 0                   b = 0                 b = 0

However, I get a totally different result:

points on an icosahedron

The points are rendered much more brightly than the expected 0.1. Using gl_FragColor = vec4(0.0, 0.2, 0.0, 0.0); the points become fully saturated (g = 255).

What causes this? I have some ideas and hopefully can push me in the right direction.

  1. Perhaps three.js appends some code to my fragment shader, modifying the gl_FragColor?
  2. Maybe I'm not aware of some extra rendering / post processing step that three.js does?
  3. Maybe I've misunderstood the math regarding blending modes?
  4. Maybe there is some implementation error in three.js?
  5. Perhaps it's a browser / canvas / color space thing? (However, I cannot imagine any transformation that causes a green value of 0.2 to become fully saturated (255) in any color space transformation!)

Any help would be greatly appreciated. Thanks!


Solution

  • The issue is every point is being drawn 5 times because IcosahedronBufferGeometry is designed for drawing triangles. Each vertex is shared by 5 triangles. We can test this by augmenting drawArrays

    Running the code below we see it's asking to draw 60 points even though visually there would only be 12 (the code below does not draw anything visible, it's only designed to check the draw call).

    WebGLRenderingContext.prototype.drawArrays = function(origFn) {
      return function(...args) {
        console.log('drawArrays', ...args);
        origFn.call(this, ...args);
      };
    }(WebGLRenderingContext.prototype.drawArrays);
    
    function main() {
      const canvas = document.querySelector('#c');
      const renderer = new THREE.WebGLRenderer({canvas});
      const camera = new THREE.Camera();
      const scene = new THREE.Scene();
      const geo = new THREE.IcosahedronBufferGeometry(1);
      const material = new THREE.MeshBasicMaterial();
      scene.add(new THREE.Points(geo, material));
      renderer.render(scene, camera);
    }
    
    main();
    <canvas id="c"></canvas>
    <script src="https://threejsfundamentals.org/threejs/resources/threejs/r112/build/three.min.js"></script>