three.js

three.js - passing vertex colors to fragment shader


I've searched through simple to complex shader examples in three.js but can't seem to find a trivial example of passing a color that vertex holds to the fragment shader. I've made a simple example where the vertex shader that just leaves vertices where they are, but also passes "color" uniform (i think i saw that somewhere but havent been used) to the fragment shader via "vColor" varying, and then in fragment shader i'm trying just to return that same color. The browser renders the triangle black, and i'm guessing the color is empty. I know that it's simple but I can't figure it out myself, and I can't figure out what does ShaderMaterial pass to the shaders that I can play with there. Can someone please explain how?

Here's the code:

<script id="vertexShader" type="x-shader/x-vertex">
    uniform vec3 color;

    varying vec3 vColor;

    void main(){
        vColor = color;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    }
</script>

<script id="fragmentShader" type="x-shader/x-fragment">
    varying vec3 vColor;

    void main(){
        gl_FragColor = vec4( vColor.rgb, 1.0 );
    }
</script>
<script>
    var container,
        camera,
        controls,
        scene,
        renderer,
        model;

    init();
    animate();

    function init() {
        camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000);
        camera.position.y = 150;
        camera.position.z = 500;

        scene = new THREE.Scene();

        var geometry = new THREE.Geometry();
        geometry.vertices.push(
            new THREE.Vector3(0, 0, 0),
            new THREE.Vector3(-120, -200, 0),
            new THREE.Vector3(120, -200, 0));

        geometry.faces.push(new THREE.Face3(0, 1, 2));

        geometry.faces[0].vertexColors[0] = new THREE.Color("rgb(255,0,0)");
        geometry.faces[0].vertexColors[1] = new THREE.Color("rgb(0,255,0)");
        geometry.faces[0].vertexColors[2] = new THREE.Color("rgb(0,0,255)");

        var materialShader = new THREE.ShaderMaterial({
            vertexShader: document.getElementById('vertexShader').textContent,
            fragmentShader: document.getElementById('fragmentShader').textContent
        });

        var materialBasic = new THREE.LineBasicMaterial({
            vertexColors: THREE.VertexColors,
            linewidth: 1
        });

        var model = new THREE.Mesh(geometry, materialShader);
        model.position.y = 150;
        scene.add(model);

        renderer = new THREE.WebGLRenderer();
        renderer.setClearColor(0xf0f0f0);
        renderer.setSize(window.innerWidth, window.innerHeight);

        container = document.createElement('div');
        document.body.appendChild(container);
        container.appendChild(renderer.domElement);
        window.addEventListener('resize', onWindowResize, false);
    }

    function onWindowResize() {
        camera.aspect = window.innerWidth / window.innerHeight;
        camera.updateProjectionMatrix();

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

    function animate() {
        requestAnimationFrame(animate);
        render();
    }

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

</script>

Notice that i have two types of materials for testing - materialShader for shaders and materialBasic as a control material that works fine. I don't have enough reputation to post images, but if stack overflow keeps uploaded images, you should see the rendered examples here: https://i.sstatic.net/tjdSt.png


Solution

  • I have solved this by playing around with variables. First mistake I made was thinking that the color (uniform vec3 color;) in the vertex shader code is uniform, but it is actually an attribute . Second, I don't have to define it, it is already defined, I've found that out when receiving a stack trace for the vertex shader, and there i figured out how the final shader GLSL code is built:

    1: precision highp float;
    2: precision highp int;
    3: 
    4: #define VERTEX_TEXTURES
    5: 
    6: 
    7: #define MAX_DIR_LIGHTS 0
    8: #define MAX_POINT_LIGHTS 0
    9: #define MAX_SPOT_LIGHTS 0
    10: #define MAX_HEMI_LIGHTS 0
    11: #define MAX_SHADOWS 0
    12: #define MAX_BONES 58
    13: 
          //a lot of blank space was printed in here, i skipped the lines
    33: 
    34: uniform mat4 modelMatrix;
    35: uniform mat4 modelViewMatrix;
    36: uniform mat4 projectionMatrix;
    37: uniform mat4 viewMatrix;
    38: uniform mat3 normalMatrix;
    39: uniform vec3 cameraPosition;
    40: attribute vec3 position;
    41: attribute vec3 normal;
    42: attribute vec2 uv;
    43: attribute vec2 uv2;
    44: #ifdef USE_COLOR
    45:     attribute vec3 color;
    46: #endif
    47: #ifdef USE_MORPHTARGETS
    48:     attribute vec3 morphTarget0;
    49:     attribute vec3 morphTarget1;
    50:     attribute vec3 morphTarget2;
    51:     attribute vec3 morphTarget3;
    52:     #ifdef USE_MORPHNORMALS
    53:         attribute vec3 morphNormal0;
    54:         attribute vec3 morphNormal1;
    55:         attribute vec3 morphNormal2;
    56:         attribute vec3 morphNormal3;
    57:     #else
    58:         attribute vec3 morphTarget4;
    59:         attribute vec3 morphTarget5;
    60:         attribute vec3 morphTarget6;
    61:         attribute vec3 morphTarget7;
    62:     #endif
    63: #endif
    64: #ifdef USE_SKINNING
    65:     attribute vec4 skinIndex;
    66:     attribute vec4 skinWeight;
    67: #endif
    68: 
    69:         
    70: 
    71:         varying vec3 vColor;
    72: 
    73:         void main(){
    74:             vColor = color;
    75:             gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
    76:         }
    77:      
    

    That means that i don't have to define the color attribute. Also, this can be seen in Three.js file, but its a concatenated string so it's hard to read through.

    Last thing I needed to do is to define vertexColors: THREE.VertexColors in the ShaderMaterial as one would in LineBasicMaterial, and I found out that by accident (in a "let's see what will happend" way).

    Here's the final code that works:

    <script id="vertexShader" type="x-shader/x-vertex">
    
    
        varying vec3 vColor;
    
        void main(){
            vColor = color;
            gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
    </script>
    
    <script id="fragmentShader" type="x-shader/x-fragment">
        varying vec3 vColor;
    
        void main(){
            gl_FragColor = vec4( vColor.rgb, 1.0 );
        }
    </script>
    <script>
        var container,
            camera,
            controls,
            scene,
            renderer,
            model;
    
        init();
        animate();
    
        function init() {
            camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 10000);
            camera.position.y = 150;
            camera.position.z = 500;
    
            scene = new THREE.Scene();
    
            var geometry  = new THREE.Geometry();
            geometry.vertices.push(
                new THREE.Vector3(0, 0, 0),
                new THREE.Vector3(-120, -200, 0),
                new THREE.Vector3(120, -200, 0));
    
            geometry.faces.push(new THREE.Face3(0, 1, 2));
    
            geometry.faces[0].vertexColors[0] = new THREE.Color("rgb(255,0,0)");
            geometry.faces[0].vertexColors[1] = new THREE.Color("rgb(0,255,0)");
            geometry.faces[0].vertexColors[2] = new THREE.Color("rgb(0,0,255)");
    
            var materialShader = new THREE.ShaderMaterial({
                vertexShader: document.getElementById('vertexShader').textContent,
                vertexColors: THREE.VertexColors,
                fragmentShader: document.getElementById('fragmentShader').textContent
            });
    
            var materialBasic = new THREE.LineBasicMaterial({
                vertexColors: THREE.VertexColors,
                linewidth: 1
            });
    
            var model = new THREE.Mesh(geometry, materialShader);
            model.position.y = 150;
            scene.add(model);
    
            renderer = new THREE.WebGLRenderer();
            renderer.setClearColor(0xf0f0f0);
            renderer.setSize(window.innerWidth, window.innerHeight);
    
            container = document.createElement('div');
            document.body.appendChild(container);
            container.appendChild(renderer.domElement);
            window.addEventListener('resize', onWindowResize, false);
        }
    
        function onWindowResize() {
            camera.aspect = window.innerWidth / window.innerHeight;
            camera.updateProjectionMatrix();
    
            renderer.setSize(window.innerWidth, window.innerHeight);
        }
    
        function animate() {
            requestAnimationFrame(animate);
            render();
        }
    
        function render() {
            renderer.render(scene, camera);
        }
    
    </script>
    

    I hope this will help someone else understand variables in the shaders. Cheers.